// Prunes omitted bundled plugin files and their unshared runtime dependencies
// from Docker-oriented production package output.
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { collectRootPackageExcludedExtensionDirs } from "./lib/bundled-plugin-build-entries.mjs";
import { removePathIfExists } from "./runtime-postbuild-shared.mjs";

const RUNTIME_DEPENDENCY_FIELDS = ["dependencies", "optionalDependencies"];

function parsePluginList(value) {
  if (typeof value !== "string") {
    return new Set();
  }
  return new Set(
    value
      .split(/[\s,]+/u)
      .map((entry) => entry.trim())
      .filter(Boolean),
  );
}

/**
 * Parses OPENCLAW_EXTENSIONS into the bundled plugin ids that Docker should keep.
 */
export function parseDockerPluginKeepList(value) {
  return parsePluginList(value);
}

function readPackageJson(filePath) {
  if (!fs.existsSync(filePath)) {
    return null;
  }
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
}

function collectRuntimeDependencyNames(packageJson) {
  const dependencies = new Set();
  for (const field of RUNTIME_DEPENDENCY_FIELDS) {
    for (const dependencyName of Object.keys(packageJson?.[field] ?? {})) {
      dependencies.add(dependencyName);
    }
  }
  return dependencies;
}

function nodeModulePath(repoRoot, packageName) {
  return path.join(repoRoot, "node_modules", ...packageName.split("/"));
}

function removeEmptyScopeDir(repoRoot, packageName) {
  if (!packageName.startsWith("@")) {
    return;
  }
  const [scope] = packageName.split("/");
  const scopeDir = path.join(repoRoot, "node_modules", scope);
  try {
    fs.rmdirSync(scopeDir);
  } catch {
    // Scope still has other packages or does not exist.
  }
}

function collectPackageRuntimeClosure(repoRoot, seedPackageNames) {
  const seen = new Set();
  const stack = [...seedPackageNames];

  while (stack.length > 0) {
    const packageName = stack.pop();
    if (!packageName || seen.has(packageName)) {
      continue;
    }
    seen.add(packageName);

    const packageJson = readPackageJson(
      path.join(nodeModulePath(repoRoot, packageName), "package.json"),
    );
    for (const dependencyName of collectRuntimeDependencyNames(packageJson)) {
      if (!seen.has(dependencyName)) {
        stack.push(dependencyName);
      }
    }
  }

  return seen;
}

function collectWorkspacePackageRuntimeSeeds(repoRoot, workspaceDir, excludedPluginIds) {
  const seeds = new Set();
  const workspaceRoot = path.join(repoRoot, workspaceDir);
  if (!fs.existsSync(workspaceRoot)) {
    return seeds;
  }

  for (const entry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) {
    if (!entry.isDirectory() || excludedPluginIds.has(entry.name)) {
      continue;
    }
    const packageJson = readPackageJson(path.join(workspaceRoot, entry.name, "package.json"));
    if (typeof packageJson?.name === "string") {
      seeds.add(packageJson.name);
    }
    for (const dependencyName of collectRuntimeDependencyNames(packageJson)) {
      seeds.add(dependencyName);
    }
  }
  return seeds;
}

function pruneNodeModulesForOmittedPlugins(repoRoot, bundledPluginDir, omittedPluginIds) {
  const rootPackageJson = readPackageJson(path.join(repoRoot, "package.json"));
  const omittedPackageNames = new Set();
  const omittedSeeds = new Set();

  for (const pluginId of omittedPluginIds) {
    const packageJson = readPackageJson(
      path.join(repoRoot, bundledPluginDir, pluginId, "package.json"),
    );
    if (typeof packageJson?.name === "string") {
      omittedPackageNames.add(packageJson.name);
    }
    for (const dependencyName of collectRuntimeDependencyNames(packageJson)) {
      omittedSeeds.add(dependencyName);
    }
  }

  const keptSeeds = new Set(collectRuntimeDependencyNames(rootPackageJson));
  for (const dependencyName of collectWorkspacePackageRuntimeSeeds(
    repoRoot,
    "packages",
    new Set(),
  )) {
    keptSeeds.add(dependencyName);
  }
  for (const dependencyName of collectWorkspacePackageRuntimeSeeds(
    repoRoot,
    bundledPluginDir,
    omittedPluginIds,
  )) {
    keptSeeds.add(dependencyName);
  }

  const keptClosure = collectPackageRuntimeClosure(repoRoot, keptSeeds);
  const omittedClosure = collectPackageRuntimeClosure(repoRoot, omittedSeeds);
  const removed = [];
  const removalCandidates = new Set([...omittedPackageNames, ...omittedClosure]);

  for (const packageName of [...removalCandidates].toSorted((left, right) =>
    left.localeCompare(right),
  )) {
    if (keptClosure.has(packageName)) {
      continue;
    }
    const packageDir = nodeModulePath(repoRoot, packageName);
    if (!fs.existsSync(packageDir)) {
      continue;
    }
    removePathIfExists(packageDir);
    removeEmptyScopeDir(repoRoot, packageName);
    removed.push(path.relative(repoRoot, packageDir).replaceAll("\\", "/"));
  }

  return removed;
}

/**
 * Removes omitted plugin dist trees plus node_modules packages not needed by kept runtime code.
 */
export function pruneDockerPluginDist(params = {}) {
  const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
  const env = params.env ?? process.env;
  const bundledPluginDir = env.OPENCLAW_BUNDLED_PLUGIN_DIR ?? "extensions";
  const keepPluginIds = parseDockerPluginKeepList(env.OPENCLAW_EXTENSIONS);
  const excludedPluginIds = collectRootPackageExcludedExtensionDirs({ cwd: repoRoot });
  const omittedPluginIds = new Set(
    [...excludedPluginIds].filter((pluginId) => !keepPluginIds.has(pluginId)),
  );
  const removed = [];

  removed.push(...pruneNodeModulesForOmittedPlugins(repoRoot, bundledPluginDir, omittedPluginIds));

  for (const pluginId of [...omittedPluginIds].toSorted((left, right) =>
    left.localeCompare(right),
  )) {
    for (const pluginPath of [
      path.join(bundledPluginDir, pluginId),
      path.join("dist", "extensions", pluginId),
      path.join("dist-runtime", "extensions", pluginId),
    ]) {
      const absolutePluginPath = path.join(repoRoot, pluginPath);
      if (!fs.existsSync(absolutePluginPath)) {
        continue;
      }
      removePathIfExists(absolutePluginPath);
      removed.push(path.relative(repoRoot, absolutePluginPath).replaceAll("\\", "/"));
    }
  }

  return removed;
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  pruneDockerPluginDist();
}
