// Determines CI scope from changed paths.
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";

/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
/** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
/** @typedef {{ runFastInstallSmoke: boolean; runFullInstallSmoke: boolean }} InstallSmokeScope */

const FULL_SCOPE = {
  runNode: true,
  runMacos: true,
  runAndroid: true,
  runWindows: true,
  runSkillsPython: true,
  runChangedSmoke: true,
  runControlUiI18n: true,
};

const EMPTY_SCOPE = {
  runNode: false,
  runMacos: false,
  runAndroid: false,
  runWindows: false,
  runSkillsPython: false,
  runChangedSmoke: false,
  runControlUiI18n: false,
};

const DOCS_PATH_RE = /^(docs\/|.*\.mdx?$)/;
const SKILLS_PYTHON_SCOPE_RE = /^(skills\/|skills\/pyproject\.toml$)/;
const INSTALL_SMOKE_WORKFLOW_SCOPE_RE = /^\.github\/workflows\/install-smoke\.yml$/;
const NATIVE_PROTOCOL_GEN_RE = /^apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\//;
const MACOS_NATIVE_RE =
  /^(apps\/macos\/|apps\/macos-mlx-tts\/|apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/)/;
const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/;
const NODE_SCOPE_RE =
  /^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|\.github\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.oxlintrc\.json$|\.oxfmtrc\.jsonc$)/;
const WINDOWS_SCOPE_RE =
  /^(src\/process\/|src\/infra\/windows-install-roots\.ts$|src\/plugins\/import-specifier(?:\.test)?\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|scripts\/(?:install\.ps1|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|js)|lib\/format-generated-module\.mjs)$|test\/scripts\/(?:format-generated-module|install-ps1|npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/ci\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/;
const WINDOWS_TEST_SCOPE_RE =
  /^(src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/windows-install-roots\.test\.ts$|src\/plugins\/import-specifier\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|test\/scripts\/(?:format-generated-module|npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/;
const TEST_ONLY_PATH_RE =
  /(^test\/|\/test\/|\/tests\/|(?:^|\/)[^/]+\.(?:test|spec|test-utils|test-support|test-harness|e2e-harness)\.[cm]?[jt]sx?$)/;
const CONTROL_UI_I18N_SCOPE_RE =
  /^(ui\/src\/i18n\/|scripts\/control-ui-i18n\.ts$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
const NATIVE_ONLY_RE =
  /^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|apps\/swabble\/|Swabble\/|appcast\.xml$)/;
const FAST_INSTALL_SMOKE_SCOPE_RE =
  /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh)$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FULL_INSTALL_SMOKE_SCOPE_RE =
  /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install(?:-cli)?\.sh$|scripts\/install\.ps1$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/(?:install-smoke|website-installer-sync)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE =
  /^(?:src\/(?:channels|gateway|plugin-sdk|plugins)\/|packages\/gateway-(?:client|protocol)\/src\/)/;
const NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE =
  /^src\/plugins\/contracts\/(?:inventory\/bundled-capability-metadata|registry|tts-contract-suites)\.ts$/;
const NODE_FAST_CI_ROUTING_SCOPE_RE =
  /^(scripts\/(?:ci-changed-scope|check-changed|run-vitest|test-projects(?:\.test-support)?)\.mjs$|scripts\/test-projects\.test-support\.d\.mts$|src\/commands\/status\.scan-result\.test\.ts$|src\/scripts\/ci-changed-scope\.test\.ts$|test\/scripts\/(?:changed-lanes|run-vitest|test-projects)\.test\.ts$|\.github\/workflows\/ci\.yml$)/;
const NODE_FAST_SCOPE_RE = new RegExp(
  `${NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.source}|${NODE_FAST_CI_ROUTING_SCOPE_RE.source}`,
);

/**
 * @param {string[]} changedPaths
 * @returns {ChangedScope}
 */
/**
 * Detects high-level CI scope from changed file paths.
 */
export function detectChangedScope(changedPaths) {
  if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
    return {
      runNode: true,
      runMacos: true,
      runAndroid: true,
      runWindows: true,
      runSkillsPython: true,
      runChangedSmoke: true,
      runControlUiI18n: true,
    };
  }

  let runNode = false;
  let runMacos = false;
  let runAndroid = false;
  let runWindows = false;
  let runSkillsPython = false;
  let runChangedSmoke = false;
  let runControlUiI18n = false;
  let hasNonDocs = false;
  let hasNonNativeNonDocs = false;

  for (const rawPath of changedPaths) {
    const path = rawPath.trim();
    if (!path) {
      continue;
    }

    if (DOCS_PATH_RE.test(path)) {
      continue;
    }

    hasNonDocs = true;

    if (SKILLS_PYTHON_SCOPE_RE.test(path)) {
      runSkillsPython = true;
    }

    if (INSTALL_SMOKE_WORKFLOW_SCOPE_RE.test(path)) {
      runChangedSmoke = true;
    }

    if (!NATIVE_PROTOCOL_GEN_RE.test(path) && MACOS_NATIVE_RE.test(path)) {
      runMacos = true;
    }

    if (!NATIVE_PROTOCOL_GEN_RE.test(path) && ANDROID_NATIVE_RE.test(path)) {
      runAndroid = true;
    }

    if (NODE_SCOPE_RE.test(path)) {
      runNode = true;
    }

    if (
      WINDOWS_SCOPE_RE.test(path) &&
      (!TEST_ONLY_PATH_RE.test(path) || WINDOWS_TEST_SCOPE_RE.test(path))
    ) {
      runWindows = true;
    }

    if (detectInstallSmokeScopeForPath(path).runFastInstallSmoke) {
      runChangedSmoke = true;
    }

    if (CONTROL_UI_I18N_SCOPE_RE.test(path)) {
      runControlUiI18n = true;
    }

    if (!NATIVE_ONLY_RE.test(path)) {
      hasNonNativeNonDocs = true;
    }
  }

  if (!runNode && hasNonDocs && hasNonNativeNonDocs) {
    runNode = true;
  }

  return {
    runNode,
    runMacos,
    runAndroid,
    runWindows,
    runSkillsPython,
    runChangedSmoke,
    runControlUiI18n,
  };
}

/**
 * @param {string[]} changedPaths
 * @returns {NodeFastScope}
 */
/**
 * Detects whether node-fast CI can cover the changed paths.
 */
export function detectNodeFastScope(changedPaths) {
  if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
    return { runFastOnly: false, runPluginContracts: false, runCiRouting: false };
  }

  let hasNonDocs = false;
  let runPluginContracts = false;
  let runCiRouting = false;

  for (const rawPath of changedPaths) {
    const path = rawPath.trim();
    if (!path || DOCS_PATH_RE.test(path)) {
      continue;
    }

    hasNonDocs = true;
    runPluginContracts ||= NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.test(path);
    runCiRouting ||= NODE_FAST_CI_ROUTING_SCOPE_RE.test(path);

    if (!NODE_FAST_SCOPE_RE.test(path)) {
      return { runFastOnly: false, runPluginContracts: false, runCiRouting: false };
    }
  }

  const runFastOnly = hasNonDocs && (runPluginContracts || runCiRouting);
  return {
    runFastOnly,
    runPluginContracts: runFastOnly && runPluginContracts,
    runCiRouting: runFastOnly && runCiRouting,
  };
}

/**
 * @param {string} path
 * @returns {InstallSmokeScope}
 */
function detectInstallSmokeScopeForPath(path) {
  const runFullInstallSmoke = FULL_INSTALL_SMOKE_SCOPE_RE.test(path);
  const runFastInstallSmoke =
    runFullInstallSmoke ||
    FAST_INSTALL_SMOKE_SCOPE_RE.test(path) ||
    (FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path));
  return { runFastInstallSmoke, runFullInstallSmoke };
}

/**
 * @param {string[]} changedPaths
 * @returns {InstallSmokeScope}
 */
/**
 * Detects whether install-smoke CI should run for changed paths.
 */
export function detectInstallSmokeScope(changedPaths) {
  if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
    return { runFastInstallSmoke: true, runFullInstallSmoke: true };
  }

  let runFastInstallSmoke = false;
  let runFullInstallSmoke = false;
  for (const rawPath of changedPaths) {
    const path = rawPath.trim();
    if (!path || DOCS_PATH_RE.test(path)) {
      continue;
    }
    const pathScope = detectInstallSmokeScopeForPath(path);
    runFastInstallSmoke ||= pathScope.runFastInstallSmoke;
    runFullInstallSmoke ||= pathScope.runFullInstallSmoke;
  }
  return { runFastInstallSmoke, runFullInstallSmoke };
}

/**
 * @param {string} base
 * @param {string} [head]
 * @param {string} [cwd]
 * @returns {string[]}
 */
/**
 * Lists changed paths for CI base/head inputs.
 */
export function listChangedPaths(
  base,
  head = "HEAD",
  cwd = process.cwd(),
  preferMergeHeadFirstParent = false,
) {
  if (!base) {
    return [];
  }
  const diffBase = resolveMergeHeadDiffBase({
    base,
    head,
    cwd,
    preferFirstParent: preferMergeHeadFirstParent,
  });
  const output = execFileSync("git", ["diff", "--name-only", diffBase, head], {
    cwd,
    stdio: ["ignore", "pipe", "pipe"],
    encoding: "utf8",
  });
  return output
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => line.length > 0);
}

/**
 * @param {ChangedScope} scope
 * @param {string} [outputPath]
 * @param {InstallSmokeScope} [installSmokeScope]
 */
/**
 * Writes CI scope decisions to GitHub Actions output.
 */
export function writeGitHubOutput(
  scope,
  outputPath = process.env.GITHUB_OUTPUT,
  installSmokeScope = {
    runFastInstallSmoke: scope.runChangedSmoke,
    runFullInstallSmoke: scope.runChangedSmoke,
  },
  nodeFastScope = { runFastOnly: false, runPluginContracts: false, runCiRouting: false },
) {
  if (!outputPath) {
    throw new Error("GITHUB_OUTPUT is required");
  }
  appendFileSync(outputPath, `run_node=${scope.runNode}\n`, "utf8");
  appendFileSync(outputPath, `run_macos=${scope.runMacos}\n`, "utf8");
  appendFileSync(outputPath, `run_android=${scope.runAndroid}\n`, "utf8");
  appendFileSync(outputPath, `run_windows=${scope.runWindows}\n`, "utf8");
  appendFileSync(outputPath, `run_skills_python=${scope.runSkillsPython}\n`, "utf8");
  appendFileSync(outputPath, `run_changed_smoke=${scope.runChangedSmoke}\n`, "utf8");
  appendFileSync(outputPath, `run_node_fast_only=${nodeFastScope.runFastOnly}\n`, "utf8");
  appendFileSync(
    outputPath,
    `run_node_fast_plugin_contracts=${nodeFastScope.runPluginContracts}\n`,
    "utf8",
  );
  appendFileSync(outputPath, `run_node_fast_ci_routing=${nodeFastScope.runCiRouting}\n`, "utf8");
  appendFileSync(
    outputPath,
    `run_fast_install_smoke=${installSmokeScope.runFastInstallSmoke}\n`,
    "utf8",
  );
  appendFileSync(
    outputPath,
    `run_full_install_smoke=${installSmokeScope.runFullInstallSmoke}\n`,
    "utf8",
  );
  appendFileSync(outputPath, `run_control_ui_i18n=${scope.runControlUiI18n}\n`, "utf8");
}

function isDirectRun() {
  return isDirectRunUrl(process.argv[1], import.meta.url);
}

/** @param {string[]} argv */
function readRefValue(argv, index, optionName) {
  const value = argv[index + 1];
  if (value === undefined || value === "" || value.startsWith("--")) {
    throw new Error(`${optionName} requires a value`);
  }
  return value;
}

/** @param {string[]} argv */
export function parseArgs(argv) {
  const args = { base: "", head: "HEAD", mergeHeadFirstParent: false };
  for (let i = 0; i < argv.length; i += 1) {
    if (argv[i] === "--base") {
      args.base = readRefValue(argv, i, "--base");
      i += 1;
      continue;
    }
    if (argv[i] === "--head") {
      args.head = readRefValue(argv, i, "--head");
      i += 1;
      continue;
    }
    if (argv[i] === "--merge-head-first-parent") {
      args.mergeHeadFirstParent = true;
    }
  }
  return args;
}

if (isDirectRun()) {
  try {
    const args = parseArgs(process.argv.slice(2));
    const changedPaths = listChangedPaths(
      args.base,
      args.head,
      process.cwd(),
      args.mergeHeadFirstParent,
    );
    if (changedPaths.length === 0) {
      writeGitHubOutput(EMPTY_SCOPE);
      process.exit(0);
    }
    writeGitHubOutput(
      detectChangedScope(changedPaths),
      process.env.GITHUB_OUTPUT,
      detectInstallSmokeScope(changedPaths),
      detectNodeFastScope(changedPaths),
    );
  } catch {
    writeGitHubOutput(FULL_SCOPE);
  }
}
