#!/usr/bin/env node
import { execFileSync } from "node:child_process";

import { readArtifactCatalogSummary } from "./src/artifact-catalog-authority.mjs";

const args = parseArgs(process.argv.slice(2));

const noDepsGitOpsOnlyPaths = Object.freeze([
  "deploy/gitops/",
  "deploy/frp/",
  "scripts/ci-plan.mjs",
  "scripts/gitops-render.mjs",
  "scripts/src/runtime-lane.ts",
  "tsconfig.gitops.json"
]);

const noDepsDocsOnlyPaths = Object.freeze([
  "docs/",
  "README.md",
  "AGENTS.md"
]);

if (args.help) {
  process.stdout.write(`${usage()}\n`);
  process.exit(0);
}

const noDepsPlan = tryCreateNoDepsPlan(args);
if (noDepsPlan) {
  const artifactCatalog = await noDepsArtifactCatalogSummary(args);
  if (catalogAllowsNoDepsReuse(artifactCatalog, args.services)) {
    noDepsPlan.artifactCatalog = artifactCatalog;
    noDepsPlan.compatibility.artifactCatalog = artifactCatalog.catalogPath;
    noDepsPlan.compatibility.artifactCatalogAuthority = artifactCatalog.authority;
    process.stdout.write(args.pretty ? `${JSON.stringify(noDepsPlan, null, 2)}\n` : `${JSON.stringify(noDepsPlan)}\n`);
    process.exit(0);
  }
}

const { createCiPlan } = await import("./src/ci-plan-lib.mjs");
const plan = await createCiPlan(args);
process.stdout.write(args.pretty ? `${JSON.stringify(plan, null, 2)}\n` : `${JSON.stringify(plan)}\n`);

function tryCreateNoDepsPlan(options) {
  const services = Array.isArray(options.services) ? options.services.filter(Boolean) : [];
  if (services.length === 0) return null;
  const targetRef = options.targetRef ?? "HEAD";
  const inferredBaseRef = options.baseRef ?? tryGitValue(["rev-parse", `${targetRef}^`]);
  const baseRef = inferredBaseRef || targetRef;
  const changedPaths = gitValue(["diff", "--name-only", baseRef, targetRef]).split("\n").map((line) => line.trim()).filter(Boolean);
  const summary = classifyNoDepsGlobalChange(changedPaths);
  if (!summary.gitopsOnly && !summary.docsOnly && !summary.testOnly && summary.summary !== "no-source-diff") return null;
  const sourceCommitId = gitValue(["rev-parse", targetRef]);
  const shortCommitId = gitValue(["rev-parse", "--short=7", targetRef]);
  return {
    planVersion: "v1",
    sourceCommitId,
    shortCommitId,
    baseRef,
    targetRef,
    compatibility: {
      deployConfig: options.deployConfigPath ?? "deploy/deploy.yaml",
      artifactCatalog: options.artifactCatalogPath ?? null,
      lane: options.lane ?? "v02",
      ciContractSource: "scripts/ci-plan.mjs",
      mode: "no-deps-global-change-fast-path",
      currentRenderCompatible: true,
      plannerMutatesDeployJson: false,
      serviceIdSource: "cli-services",
      defaultComponentLazyBuild: true,
      envReuseCodeOnlyFastLane: true,
      fullBuildFallback: false
    },
    inputs: {
      registryPrefix: options.registryPrefix ?? process.env.HWLAB_DEV_REGISTRY_PREFIX ?? process.env.HWLAB_DEV_REGISTRY ?? "127.0.0.1:5000/hwlab",
      serviceCount: services.length,
      verifyReuseRegistry: options.verifyReuseRegistry ?? booleanEnv(process.env.HWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY)
    },
    changedPaths,
    changedPathSummary: summary,
    imageBuildRequired: false,
    affectedServices: [],
    rolloutServices: [],
    buildServices: [],
    rolloutWithoutImageBuildServices: [],
    reusedServices: services,
    serviceReusedCount: services.length,
    imageBuildSkippedServices: services,
    buildSkippedCount: services.length,
    services: services.map((serviceId) => ({
      serviceId,
      affected: false,
      buildRequired: false,
      skipImageBuild: true,
      runtimeMode: "env-reuse-git-mirror-checkout",
      envReuse: true,
      envChanged: false,
      codeChanged: false,
      reason: [summary.summary === "no-source-diff" ? "no-source-diff" : `${summary.summary}-no-image-build`]
    })),
    ciCdPlan: {
      willBuild: [],
      willRollout: [],
      willRolloutWithoutImageBuild: [],
      willReuse: services,
      willRunGitopsPromote: summary.gitopsOnly,
      noImageBuildReason: summary.summary
    }
  };
}

async function noDepsArtifactCatalogSummary(options) {
  const catalogPath = options.artifactCatalogPath;
  if (!catalogPath) {
    return {
      status: "missing",
      authority: "none",
      catalogPath: null,
      authorityPath: null,
      gitopsBranch: null,
      gitopsCommitId: null,
      catalogSourceCommitId: null,
      serviceCount: 0,
      catalogSha256: null,
      coverage: null,
      bootstrapReason: null
    };
  }
  return readArtifactCatalogSummary({ repoRoot: process.cwd(), catalogPath });
}

function catalogAllowsNoDepsReuse(artifactCatalog, selectedServices) {
  if (artifactCatalog?.status !== "hydrated" || artifactCatalog.authority !== "gitops-branch") return false;
  const catalogServiceIds = artifactCatalog.coverage?.catalogServiceIds;
  if (!Array.isArray(catalogServiceIds)) return false;
  return selectedServices.every((serviceId) => catalogServiceIds.includes(serviceId));
}

function classifyNoDepsGlobalChange(changedPaths) {
  const relevant = changedPaths.filter(Boolean);
  const testOnly = relevant.length > 0 && relevant.every(isNoDepsTestOnlyPath);
  const docsOnly = relevant.length > 0 && relevant.every((item) => isNoDepsDocsOnlyPath(item) || isNoDepsTestOnlyPath(item));
  const gitopsOnly = relevant.length > 0 && relevant.every((item) => isNoDepsGitOpsOnlyPath(item) || isNoDepsDocsOnlyPath(item) || isNoDepsTestOnlyPath(item));
  return {
    count: relevant.length,
    testOnly,
    docsOnly,
    gitopsOnly,
    summary: relevant.length === 0
      ? "no-source-diff"
      : testOnly
        ? "test-only-change"
        : docsOnly
          ? "docs-only-change"
          : gitopsOnly
            ? "gitops-only-change"
            : "runtime-or-build-change"
  };
}

function isNoDepsGitOpsOnlyPath(filePath) {
  return noDepsGitOpsOnlyPaths.some((pattern) => pathMatches(pattern, filePath));
}

function isNoDepsDocsOnlyPath(filePath) {
  const normalized = normalizeRepoPath(filePath);
  if (normalized.startsWith("internal/agent/prompts/")) return false;
  if (normalized.startsWith("skills/")) return false;
  return noDepsDocsOnlyPaths.some((pattern) => pathMatches(pattern, normalized)) || normalized.endsWith(".md");
}

function isNoDepsTestOnlyPath(filePath) {
  const normalized = normalizeRepoPath(filePath);
  return /\.test\.(mjs|ts)$/u.test(normalized) || normalized.includes("/__tests__/") || normalized.includes("/fixtures/");
}

function pathMatches(pattern, filePath) {
  const normalizedPattern = normalizeRepoPath(pattern);
  const normalizedFilePath = normalizeRepoPath(filePath);
  if (!normalizedPattern || !normalizedFilePath) return false;
  if (normalizedPattern.endsWith("/")) return normalizedFilePath.startsWith(normalizedPattern);
  return normalizedFilePath === normalizedPattern;
}

function normalizeRepoPath(value) {
  return String(value ?? "").replaceAll("\\", "/").replace(/^\.\//u, "").replace(/^\/+/, "").trim();
}

function gitValue(args) {
  return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 }).trim();
}

function tryGitValue(args) {
  try {
    return gitValue(args);
  } catch {
    return "";
  }
}

function booleanEnv(value) {
  return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
}

function parseArgs(argv) {
  const parsed = { pretty: false };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--base-ref") parsed.baseRef = readOption(argv, ++index, arg);
    else if (arg === "--target-ref") parsed.targetRef = readOption(argv, ++index, arg);
    else if (arg === "--deploy-config") parsed.deployConfigPath = readOption(argv, ++index, arg);
    else if (arg === "--artifact-catalog") parsed.artifactCatalogPath = readOption(argv, ++index, arg);
    else if (arg === "--lane") parsed.lane = readOption(argv, ++index, arg);
    else if (arg === "--registry-prefix") parsed.registryPrefix = readOption(argv, ++index, arg);
    else if (arg === "--verify-reuse-registry") parsed.verifyReuseRegistry = true;
    else if (arg === "--no-verify-reuse-registry") parsed.verifyReuseRegistry = false;
    else if (arg === "--base-image") parsed.baseImage = readOption(argv, ++index, arg);
    else if (arg === "--services") parsed.services = readOption(argv, ++index, arg).split(",").map((item) => item.trim()).filter(Boolean);
    else if (arg === "--pretty") parsed.pretty = true;
    else if (arg === "--help" || arg === "-h") parsed.help = true;
    else throw new Error(`unknown argument ${arg}`);
  }
  return parsed;
}

function readOption(argv, index, name) {
  const value = argv[index];
  if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function usage() {
  return [
    "usage: node scripts/ci-plan.mjs [--lane v02|v03] [--base-ref REF] [--target-ref REF] [--verify-reuse-registry] [--pretty]",
    "",
    "Plan node monorepo image builds without mutating CI/CD state.",
    "For runtime lane env reuse, service component boundaries and env recipe must come from deploy/deploy.yaml.",
    "",
    "examples:",
    "  node scripts/ci-plan.mjs --lane v02 --base-ref HEAD~1 --target-ref HEAD --pretty",
    "  node scripts/ci-plan.mjs --lane v03 --base-ref HEAD~1 --target-ref HEAD --pretty",
    "  node scripts/ci-plan.mjs --services hwlab-cloud-api,hwlab-cloud-web --pretty"
  ].join("\n");
}
