// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. options module for scripts/src/hwlab-g14.ts.

// Moved mechanically from scripts/src/hwlab-g14.ts:289-638 for #903.

import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { createHash, randomBytes } from "node:crypto";
import { repoRoot, rootPath, type Config } from "../config";
import { runCommand } from "../command";
import { cancelJob, listJobs, readJob, startJob } from "../jobs";
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";

import type { G14ControlPlaneOptions, G14GitMirrorOptions, G14LegacyRetirementOptions, G14MonitorLane, G14MonitorOptions, G14ObservabilityOptions, G14RecordRolloutOptions, G14SecretOptions, G14ToolsImageOptions, G14UpstreamImageOptions } from "./types";
import { positiveIntegerOption, runtimeLaneMasterAdminApiKeySecretName, runtimeLaneOpenFgaSecretName } from "./remote";
import { DEFAULT_INTERVAL_SECONDS, DEFAULT_MAX_CYCLES, DEFAULT_TIMEOUT_SECONDS, G14_CI_TOOLS_BASE_TAG, V02_MASTER_ADMIN_API_KEY_SECRET, V02_MASTER_ADMIN_API_KEY_SECRET_KEY, V02_OPENFGA_AUTHN_SECRET_KEY, V02_OPENFGA_DATASTORE_URI_SECRET_KEY, V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY, V02_OPENFGA_SECRET } from "./types";

export function hwlabG14MonitorStateFileName(options: Pick<G14MonitorOptions, "once" | "dryRun"> & { lane?: G14MonitorLane }): string {
  const prefix = options.lane !== undefined && options.lane !== "g14" ? `latest-${options.lane}-` : "latest-";
  if (options.once && options.dryRun) return `${prefix}once-dry-run-job.json`;
  if (options.once) return `${prefix}once-job.json`;
  if (options.dryRun) return `${prefix}dry-run-job.json`;
  return `${prefix}monitor-job.json`;
}

export function hwlabG14MonitorStateRole(options: Pick<G14MonitorOptions, "once" | "dryRun"> & { lane?: G14MonitorLane }): string {
  const lanePrefix = options.lane !== undefined && options.lane !== "g14" ? `${options.lane}-` : "";
  if (options.once && options.dryRun) return `${lanePrefix}once-dry-run`;
  if (options.once) return `${lanePrefix}once`;
  if (options.dryRun) return `${lanePrefix}dry-run-monitor`;
  return `${lanePrefix}monitor`;
}

export function parseMonitorLane(args: string[]): G14MonitorLane {
  const lane = optionValue(args, "--lane") ?? "g14";
  if (lane !== "g14" && !isHwlabRuntimeLane(lane)) throw new Error(`monitor-prs --lane must be g14 or ${hwlabRuntimeLaneIds().join("|")}`);
  return lane;
}

export function parseOptions(args: string[]): G14MonitorOptions {
  return {
    lane: parseMonitorLane(args),
    intervalSeconds: positiveIntegerOption(args, "--interval-seconds", DEFAULT_INTERVAL_SECONDS, 86400),
    maxCycles: positiveIntegerOption(args, "--max-cycles", DEFAULT_MAX_CYCLES, 100000),
    once: args.includes("--once"),
    dryRun: args.includes("--dry-run"),
    worker: args.includes("--worker"),
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", DEFAULT_TIMEOUT_SECONDS, 86400),
  };
}

export function optionValue(args: string[], name: string): string | undefined {
  const index = args.indexOf(name);
  if (index === -1) return undefined;
  const value = args[index + 1];
  if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

export function validateFullShaOption(value: string, name: string): string {
  if (!/^[0-9a-f]{40}$/iu.test(value)) throw new Error(`${name} must be a full 40-character git SHA`);
  return value.toLowerCase();
}

export function validateV02PipelineRunOption(value: string): string {
  if (!/^hwlab-v02-ci-poll-[0-9a-f]{7,40}$/iu.test(value)) throw new Error("--pipeline-run must be a hwlab-v02-ci-poll-<sha> PipelineRun name");
  return value.toLowerCase();
}

export function validateRuntimeLanePipelineRunOption(lane: HwlabRuntimeLane, value: string): string {
  const spec = hwlabRuntimeLaneSpec(lane);
  const escapedPrefix = spec.pipelineRunPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
  if (!new RegExp(`^${escapedPrefix}-[0-9a-f]{7,40}(?:-[a-z0-9][a-z0-9-]{0,24})?$`, "iu").test(value)) {
    throw new Error(`--pipeline-run must be a ${spec.pipelineRunPrefix}-<sha>[-rerun] PipelineRun name for --lane ${lane}`);
  }
  return value.toLowerCase();
}

export function parseRecordRolloutOptions(args: string[]): G14RecordRolloutOptions {
  const prRaw = optionValue(args, "--pr") ?? optionValue(args, "--number");
  const prNumber = Number(prRaw);
  if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("record-rollout requires --pr <number>");
  return {
    prNumber,
    sourceCommit: optionValue(args, "--source-commit"),
    pipelineRun: optionValue(args, "--pipeline-run"),
    gitopsRevision: optionValue(args, "--gitops-revision"),
    mergedAt: optionValue(args, "--merged-at"),
    pipelineSucceededAt: optionValue(args, "--pipeline-succeeded-at"),
    finishedAt: optionValue(args, "--finished-at"),
    dryRun: args.includes("--dry-run"),
  };
}

export function parseControlPlaneOptions(args: string[]): G14ControlPlaneOptions {
  const [rawAction] = args;
  const actionRaw = rawAction === "rerun-current" ? "trigger-current" : rawAction;
  if (
    actionRaw !== "status" &&
    actionRaw !== "closeout" &&
    actionRaw !== "apply" &&
    actionRaw !== "trigger-current" &&
    actionRaw !== "refresh" &&
    actionRaw !== "cleanup-runs" &&
    actionRaw !== "cleanup-released-pvs" &&
    actionRaw !== "runtime-migration"
  ) {
    throw new Error("control-plane usage: status|apply|trigger-current|refresh --lane v02|v03 | closeout|runtime-migration --lane v02 | cleanup-runs --lane v02|v03|g14|all | cleanup-released-pvs --lane all [--dry-run|--confirm]");
  }
  const laneRaw = optionValue(args, "--lane") ?? (actionRaw === "cleanup-released-pvs" ? "all" : "v02");
  let lane: G14ControlPlaneOptions["lane"];
  if (actionRaw === "cleanup-runs") {
    if (laneRaw !== "g14" && laneRaw !== "all" && !isHwlabRuntimeLane(laneRaw)) throw new Error("control-plane cleanup-runs requires --lane v02|v03|g14|all");
    lane = laneRaw;
  } else if (actionRaw === "cleanup-released-pvs") {
    if (laneRaw !== "all") throw new Error("control-plane cleanup-released-pvs requires --lane all because released PVs no longer preserve the v02/g14 PipelineRun lane");
    lane = laneRaw;
  } else if (actionRaw === "closeout" || actionRaw === "runtime-migration") {
    if (laneRaw !== "v02") throw new Error("control-plane closeout/runtime-migration currently requires --lane v02");
    lane = laneRaw;
  } else if (!isHwlabRuntimeLane(laneRaw)) {
    throw new Error(`control-plane ${actionRaw} requires --lane ${hwlabRuntimeLaneIds().join("|")}`);
  } else {
    lane = laneRaw;
  }
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("control-plane accepts only one of --confirm or --dry-run");
  const allowLiveDbRead = args.includes("--allow-live-db-read");
  if (allowLiveDbRead && confirm) throw new Error("control-plane runtime-migration accepts --allow-live-db-read only with dry-run/source-check mode, not --confirm");
  if (allowLiveDbRead && actionRaw !== "runtime-migration") throw new Error("--allow-live-db-read is only valid for control-plane runtime-migration");
  const sourceCommitRaw = optionValue(args, "--source-commit");
  const pipelineRunRaw = optionValue(args, "--pipeline-run");
  if ((sourceCommitRaw !== undefined || pipelineRunRaw !== undefined) && actionRaw !== "status" && actionRaw !== "closeout" && actionRaw !== "cleanup-runs") {
    throw new Error("--source-commit and --pipeline-run are only valid for control-plane status/closeout/cleanup-runs");
  }
  if (actionRaw === "closeout" && sourceCommitRaw === undefined && pipelineRunRaw === undefined) {
    throw new Error("control-plane closeout requires --source-commit <full-sha> or --pipeline-run hwlab-v02-ci-poll-<sha>");
  }
  if (sourceCommitRaw !== undefined && pipelineRunRaw !== undefined) throw new Error("control-plane status/closeout/cleanup-runs accepts only one of --source-commit or --pipeline-run");
  return {
    action: actionRaw,
    lane,
    confirm,
    wait: args.includes("--wait"),
    rerun: args.includes("--rerun") || rawAction === "rerun-current",
    allowLiveDbRead,
    dryRun: actionRaw === "status" || actionRaw === "closeout" ? true : explicitDryRun || !confirm,
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
    minAgeMinutes: positiveIntegerOption(args, "--min-age-minutes", 60, 10080),
    limit: positiveIntegerOption(args, "--limit", 20, 200),
    sourceCommit: sourceCommitRaw === undefined ? undefined : validateFullShaOption(sourceCommitRaw, "--source-commit"),
    pipelineRun: pipelineRunRaw === undefined
      ? undefined
      : isHwlabRuntimeLane(lane)
        ? validateRuntimeLanePipelineRunOption(lane, pipelineRunRaw)
        : validateV02PipelineRunOption(pipelineRunRaw),
    history: args.includes("--history"),
  };
}

export function parseLegacyRetirementOptions(args: string[]): G14LegacyRetirementOptions {
  const [actionRaw = "status"] = args;
  if (actionRaw !== "status" && actionRaw !== "plan" && actionRaw !== "execute") {
    throw new Error("retirement usage: status|plan|execute [--dry-run|--confirm] [--wait] [--reason text] [--timeout-seconds N]");
  }
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("retirement accepts only one of --confirm or --dry-run");
  if (confirm && actionRaw !== "execute") throw new Error("--confirm is only valid for retirement execute");
  if (args.includes("--wait") && actionRaw !== "execute") throw new Error("--wait is only valid for retirement execute");
  const reason = optionValue(args, "--reason") ?? "user requested immediate retirement of legacy G14 DEV/PROD";
  if (reason.length > 240) throw new Error("--reason is limited to 240 characters");
  return {
    action: actionRaw,
    confirm,
    wait: args.includes("--wait"),
    dryRun: actionRaw !== "execute" || explicitDryRun || !confirm,
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "execute" ? 60 : 30, 300),
    reason,
  };
}

export function parseToolsImageOptions(args: string[]): G14ToolsImageOptions {
  const [actionRaw] = args;
  if (actionRaw !== "status" && actionRaw !== "build") {
    throw new Error("tools-image usage: status|build --name ci-node-tools --tag <tag> [--dockerfile path] [--dry-run|--confirm]");
  }
  const name = optionValue(args, "--name") ?? "ci-node-tools";
  if (name !== "ci-node-tools") throw new Error("tools-image currently supports --name ci-node-tools");
  const tag = optionValue(args, "--tag") ?? G14_CI_TOOLS_BASE_TAG;
  if (!/^[A-Za-z0-9_.-]+$/u.test(tag)) throw new Error("--tag may only contain letters, numbers, dot, underscore, and dash");
  const dockerfile = optionValue(args, "--dockerfile") ?? "deploy/ci/hwlab-ci-node-tools.Dockerfile";
  if (dockerfile.startsWith("/") || dockerfile.includes("..")) throw new Error("--dockerfile must be a repo-relative path without '..'");
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("tools-image accepts only one of --confirm or --dry-run");
  return {
    action: actionRaw,
    name,
    tag,
    dockerfile,
    confirm,
    dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 600, 1800),
  };
}

export function parseUpstreamImageOptions(args: string[]): G14UpstreamImageOptions {
  const [actionRaw] = args;
  if (actionRaw !== "status" && actionRaw !== "ensure") {
    throw new Error("upstream-image usage: status|ensure --name openfga --tag v1.17.0 [--dry-run|--confirm]");
  }
  const name = optionValue(args, "--name") ?? "openfga";
  if (name !== "openfga") throw new Error("upstream-image currently supports --name openfga");
  const tag = optionValue(args, "--tag") ?? "v1.17.0";
  if (tag !== "v1.17.0") throw new Error("upstream-image currently supports OpenFGA tag v1.17.0");
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("upstream-image accepts only one of --confirm or --dry-run");
  return {
    action: actionRaw,
    name,
    tag,
    confirm,
    dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 600, 1800),
  };
}

export function parseGitMirrorOptions(args: string[]): G14GitMirrorOptions {
  const [actionRaw] = args;
  if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "sync" && actionRaw !== "flush") {
    throw new Error("git-mirror usage: status|apply|sync|flush [--lane v02|v03] [--dry-run|--confirm]");
  }
  const laneRaw = optionValue(args, "--lane") ?? "v02";
  if (!isHwlabRuntimeLane(laneRaw)) throw new Error(`git-mirror --lane must be ${hwlabRuntimeLaneIds().join("|")}`);
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("git-mirror accepts only one of --confirm or --dry-run");
  return {
    action: actionRaw,
    lane: laneRaw,
    confirm,
    wait: args.includes("--wait"),
    dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "sync" || actionRaw === "flush" ? 300 : 120, 900),
  };
}

export function parseObservabilityOptions(args: string[]): G14ObservabilityOptions {
  const [actionRaw] = args;
  if (
    actionRaw !== "status" &&
    actionRaw !== "apply" &&
    actionRaw !== "query" &&
    actionRaw !== "targets" &&
    actionRaw !== "boundary" &&
    actionRaw !== "closeout"
  ) {
    throw new Error("observability usage: status|apply|query|targets|boundary|closeout [--lane v02] [--promql <expr>] [--expect-count N] [--expect-value V] [--dry-run|--confirm]");
  }
  const valueOptions = new Set(["--lane", "--promql", "--query", "--expect-count", "--expect-value", "--timeout-seconds"]);
  const booleanOptions = new Set(["--confirm", "--dry-run"]);
  for (let i = 1; i < args.length; i += 1) {
    const arg = args[i];
    if (valueOptions.has(arg)) {
      i += 1;
      if (i >= args.length || args[i].startsWith("--")) throw new Error(`${arg} requires a value`);
      continue;
    }
    if (booleanOptions.has(arg)) continue;
    if (arg === "--wait") throw new Error("observability does not support --wait; commands are bounded short CLI calls");
    if (arg.startsWith("--")) throw new Error(`unsupported observability option: ${arg}`);
    throw new Error(`unexpected observability argument: ${arg}`);
  }
  const lane = optionValue(args, "--lane") ?? "v02";
  if (lane !== "v02") throw new Error("observability currently supports --lane v02");
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("observability accepts only one of --confirm or --dry-run");
  if ((confirm || explicitDryRun) && actionRaw !== "apply") throw new Error("--confirm and --dry-run are only supported by observability apply");
  const query = optionValue(args, "--promql") ?? optionValue(args, "--query") ?? 'up{namespace="hwlab-v02"}';
  if (query.length > 500) throw new Error("--promql is limited to 500 characters");
  if (query.includes("\n") || query.includes("\r")) throw new Error("--promql must be a single-line expression");
  const expectCountRaw = optionValue(args, "--expect-count");
  const expectCount = expectCountRaw === undefined ? undefined : Number(expectCountRaw);
  if (expectCountRaw !== undefined && (!Number.isInteger(expectCount) || expectCount < 0)) throw new Error("--expect-count must be a non-negative integer");
  const expectValue = optionValue(args, "--expect-value");
  if ((expectCount !== undefined || expectValue !== undefined) && actionRaw !== "query") throw new Error("--expect-count and --expect-value are only supported by observability query");
  if (expectValue !== undefined && expectValue.trim().length === 0) throw new Error("--expect-value must not be blank");
  return {
    action: actionRaw,
    lane,
    confirm,
    wait: args.includes("--wait"),
    dryRun: actionRaw === "apply" ? explicitDryRun || !confirm : true,
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "apply" ? 240 : 120, 900),
    query,
    expectCount,
    expectValue,
  };
}

export function parseSecretOptions(args: string[]): G14SecretOptions {
  const [actionRaw] = args;
  if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "delete") {
    throw new Error("secret usage: status|ensure --lane v02|v03 --name hwlab-v0x-openfga|hwlab-v0x-master-server-admin-api-key [--dry-run|--confirm] | delete --lane v02 --name <obsolete-secret> [--dry-run|--confirm]");
  }
  const laneRaw = optionValue(args, "--lane") ?? "v02";
  if (!isHwlabRuntimeLane(laneRaw)) throw new Error(`secret --lane must be one of ${hwlabRuntimeLaneIds().join(", ")}`);
  const lane = laneRaw;
  const spec = hwlabRuntimeLaneSpec(lane);
  const openFgaSecret = runtimeLaneOpenFgaSecretName(spec);
  const masterAdminSecret = runtimeLaneMasterAdminApiKeySecretName(spec);
  const name = optionValue(args, "--name") ?? openFgaSecret;
  const key = optionValue(args, "--key");
  const confirm = args.includes("--confirm");
  const explicitDryRun = args.includes("--dry-run");
  if (confirm && explicitDryRun) throw new Error("secret accepts only one of --confirm or --dry-run");
  if (actionRaw === "delete") {
    if (lane !== "v02") throw new Error("secret delete is currently limited to --lane v02 obsolete cleanup");
    if (key !== undefined) throw new Error("secret delete does not accept --key; it deletes a whole obsolete Secret object");
    if (!/^hwlab-v02-[a-z0-9-]+$/u.test(name)) throw new Error("secret delete requires a hwlab-v02-* Secret name");
    if (name === V02_OPENFGA_SECRET || name === V02_MASTER_ADMIN_API_KEY_SECRET || name === "hwlab-v02-postgres") throw new Error(`secret delete refuses required v0.2 Secret ${name}`);
    return {
      action: actionRaw,
      lane,
      confirm,
      dryRun: explicitDryRun || !confirm,
      name,
      preset: "generic-delete",
      timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
    };
  }
  if (name === masterAdminSecret) {
    if (key !== undefined && key !== V02_MASTER_ADMIN_API_KEY_SECRET_KEY) throw new Error(`secret ${masterAdminSecret} supports only key ${V02_MASTER_ADMIN_API_KEY_SECRET_KEY}`);
    return {
      action: actionRaw,
      lane,
      confirm,
      dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
      name,
      key: key ?? V02_MASTER_ADMIN_API_KEY_SECRET_KEY,
      preset: "master-server-admin-api-key",
      timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
    };
  }
  if (name !== openFgaSecret) {
    throw new Error(`secret status/ensure currently supports only --name ${openFgaSecret} or ${masterAdminSecret} for --lane ${lane}; use secret delete for obsolete v02 Secret objects`);
  }
  if (key !== undefined && key !== V02_OPENFGA_AUTHN_SECRET_KEY && key !== V02_OPENFGA_DATASTORE_URI_SECRET_KEY && key !== V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY) {
    throw new Error(`secret ${openFgaSecret} supports keys ${V02_OPENFGA_AUTHN_SECRET_KEY}, ${V02_OPENFGA_DATASTORE_URI_SECRET_KEY}, and ${V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY}`);
  }
  if (lane !== "v02" && key === V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY) throw new Error(`${openFgaSecret}/${V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY} is derived from ${postgresSecret}/POSTGRES_PASSWORD`);
  return {
    action: actionRaw,
    lane,
    confirm,
    dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
    name,
    key,
    preset: "openfga",
    timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
  };
}
