import { createHash } from "node:crypto";
import { createReadStream, existsSync, lstatSync, readdirSync } from "node:fs";
import { relative, resolve, sep } from "node:path";

import { resolveSelfMediaCutoverTarget, resolveSelfMediaDeliveryTarget, selfMediaConfigRef, type SelfMediaCutoverTarget } from "./selfmedia-config";

type CutoverAction = "plan" | "prepare" | "status" | "rollback";

interface CutoverOptions {
  readonly targetId: string | null;
  readonly dryRun: boolean;
  readonly confirm: boolean;
}

interface SourceDataSummary {
  readonly available: boolean;
  readonly files: number;
  readonly bytes: number;
  readonly digest: string | null;
  readonly digestBasis: "relative-path,size,sha256-content";
  readonly missingPaths: readonly string[];
  readonly excludedPaths: readonly string[];
  readonly valuesPrinted: false;
}

export async function runSelfMediaCommand(args: string[]): Promise<Record<string, unknown>> {
  if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") return help();
  if (args[0] !== "cutover") return { ok: false, error: "unsupported-selfmedia-command", args, help: help() };
  const action = args[1];
  if (action !== "plan" && action !== "prepare" && action !== "status" && action !== "rollback") {
    return { ok: false, error: "unsupported-selfmedia-cutover-command", args, help: help() };
  }
  const options = parseOptions(args.slice(2));
  const target = resolveSelfMediaCutoverTarget(options.targetId);
  const delivery = resolveSelfMediaDeliveryTarget(target.id);
  if (action === "plan") return cutoverPlan(target, delivery.route);
  if (action === "status") return await cutoverStatus(target, delivery.route);
  if (action === "prepare") return await cutoverPrepare(target, delivery.route, options);
  return cutoverRollback(target, delivery.route, options);
}

function help(): Record<string, unknown> {
  return {
    ok: true,
    command: "selfmedia cutover plan|prepare|status|rollback",
    configTruth: selfMediaConfigRef,
    usage: [
      "bun scripts/cli.ts selfmedia cutover plan --target NC01",
      "bun scripts/cli.ts selfmedia cutover prepare --target NC01 --dry-run",
      "bun scripts/cli.ts selfmedia cutover status --target NC01",
      "bun scripts/cli.ts selfmedia cutover rollback --target NC01 --dry-run",
    ],
    boundary: "当前入口只做配置校验、源数据摘要和切换计划；不停止服务、不复制数据、不访问 k8s、不切换端口。",
    valuesPrinted: false,
  };
}

function parseOptions(args: string[]): CutoverOptions {
  let targetId: string | null = null;
  let dryRun = false;
  let confirm = false;
  for (let index = 0; index < args.length; index += 1) {
    const token = args[index];
    if (token === "--target") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
      targetId = value;
      index += 1;
      continue;
    }
    if (token === "--dry-run") {
      dryRun = true;
      continue;
    }
    if (token === "--confirm") {
      confirm = true;
      continue;
    }
    if (token === "--json") continue;
    throw new Error(`unsupported selfmedia cutover option ${token}`);
  }
  if (dryRun && confirm) throw new Error("--dry-run and --confirm are mutually exclusive");
  return { targetId, dryRun, confirm };
}

function cutoverPlan(target: SelfMediaCutoverTarget, deliveryRoute: string): Record<string, unknown> {
  return {
    ok: true,
    action: "selfmedia-cutover-plan",
    target: target.id,
    mode: target.mode,
    mutation: false,
    configTruth: `${selfMediaConfigRef}#cutover.targets.${target.id}`,
    routeAligned: target.destination.route === deliveryRoute,
    source: {
      workspace: target.source.workspace,
      healthUrl: target.source.healthUrl,
      dataPaths: target.source.dataPaths,
      excludes: target.source.excludes,
    },
    destination: target.destination,
    phases: target.prepare.phases,
    rollback: {
      phases: target.rollback.phases,
      dataPolicy: target.rollback.dataPolicy,
    },
    safety: {
      requiresFinalConfirmation: target.prepare.requiresFinalConfirmation,
      oldLifecycleStateExcluded: target.source.excludes.includes(".state/server.json"),
      runtimeExecutorAvailable: false,
    },
    next: `bun scripts/cli.ts selfmedia cutover prepare --target ${target.id} --dry-run`,
    valuesPrinted: false,
  };
}

async function cutoverStatus(target: SelfMediaCutoverTarget, deliveryRoute: string): Promise<Record<string, unknown>> {
  const sourceData = await summarizeSourceData(target);
  return {
    ok: sourceData.available,
    action: "selfmedia-cutover-status",
    target: target.id,
    mutation: false,
    configReady: target.destination.route === deliveryRoute && target.source.excludes.includes(".state/server.json"),
    sourceWorkspacePresent: existsSync(target.source.workspace),
    sourceData,
    runtimeObservation: {
      status: "not-requested",
      reason: "read-only CLI does not contact the host service, k8s, Argo, or public endpoint",
    },
    next: sourceData.available
      ? `bun scripts/cli.ts selfmedia cutover prepare --target ${target.id} --dry-run`
      : "Restore or select the YAML-declared source workspace before preparing cutover.",
    valuesPrinted: false,
  };
}

async function cutoverPrepare(target: SelfMediaCutoverTarget, deliveryRoute: string, options: CutoverOptions): Promise<Record<string, unknown>> {
  const sourceData = await summarizeSourceData(target);
  const confirmedButUnsupported = options.confirm;
  return {
    ok: sourceData.available && !confirmedButUnsupported,
    action: "selfmedia-cutover-prepare",
    target: target.id,
    mode: options.dryRun || !options.confirm ? "dry-run" : "blocked-confirmed",
    mutation: false,
    configReady: target.destination.route === deliveryRoute && target.source.excludes.includes(".state/server.json"),
    sourceData,
    proposedPhases: target.prepare.phases,
    portHandoff: {
      port: 4317,
      oldServiceStopCommand: target.source.stopCommand,
      destinationHealthUrl: target.destination.healthUrl,
      executed: false,
    },
    blocked: confirmedButUnsupported ? {
      code: "selfmedia-cutover-runtime-executor-not-implemented",
      reason: "本次交付只建立 YAML 所有权和只读/干运行入口，禁止从此命令执行真实切换。",
    } : null,
    next: confirmedButUnsupported
      ? "由后续受控执行器实现 PVC seed、最终增量、端口释放、Argo 同步和原入口验收后再开放 --confirm。"
      : `bun scripts/cli.ts selfmedia cutover status --target ${target.id}`,
    valuesPrinted: false,
  };
}

function cutoverRollback(target: SelfMediaCutoverTarget, deliveryRoute: string, options: CutoverOptions): Record<string, unknown> {
  const confirmedButUnsupported = options.confirm;
  return {
    ok: !confirmedButUnsupported,
    action: "selfmedia-cutover-rollback",
    target: target.id,
    mode: options.dryRun || !options.confirm ? "dry-run" : "blocked-confirmed",
    mutation: false,
    configReady: target.destination.route === deliveryRoute,
    proposedPhases: target.rollback.phases,
    dataPolicy: target.rollback.dataPolicy,
    blocked: confirmedButUnsupported ? {
      code: "selfmedia-cutover-runtime-executor-not-implemented",
      reason: "回滚命令当前只输出计划，不操作 Argo、Deployment、宿主进程或数据。",
    } : null,
    valuesPrinted: false,
  };
}

async function summarizeSourceData(target: SelfMediaCutoverTarget): Promise<SourceDataSummary> {
  const root = resolve(target.source.workspace);
  const missingPaths: string[] = [];
  const files: { absolute: string; relative: string; size: number }[] = [];
  for (const declared of target.source.dataPaths) {
    const absolute = resolve(root, declared);
    if (!inside(root, absolute) || !existsSync(absolute)) {
      missingPaths.push(declared);
      continue;
    }
    collectFiles(root, absolute, target.source.excludes, files);
  }
  files.sort((left, right) => left.relative.localeCompare(right.relative));
  const digest = createHash("sha256");
  let bytes = 0;
  for (const file of files) {
    const contentDigest = await hashFile(file.absolute);
    bytes += file.size;
    digest.update(file.relative);
    digest.update("\0");
    digest.update(String(file.size));
    digest.update("\0");
    digest.update(contentDigest);
    digest.update("\n");
  }
  return {
    available: missingPaths.length === 0,
    files: files.length,
    bytes,
    digest: missingPaths.length === 0 ? `sha256:${digest.digest("hex")}` : null,
    digestBasis: "relative-path,size,sha256-content",
    missingPaths,
    excludedPaths: target.source.excludes,
    valuesPrinted: false,
  };
}

function collectFiles(root: string, current: string, excludes: readonly string[], output: { absolute: string; relative: string; size: number }[]): void {
  const currentRelative = relative(root, current).split(sep).join("/");
  if (isExcluded(currentRelative, excludes)) return;
  const stat = lstatSync(current);
  if (stat.isSymbolicLink()) return;
  if (stat.isFile()) {
    output.push({ absolute: current, relative: currentRelative, size: stat.size });
    return;
  }
  if (!stat.isDirectory()) return;
  for (const entry of readdirSync(current).sort()) collectFiles(root, resolve(current, entry), excludes, output);
}

function isExcluded(path: string, excludes: readonly string[]): boolean {
  return excludes.some((excluded) => path === excluded || path.startsWith(`${excluded}/`));
}

function inside(root: string, candidate: string): boolean {
  return candidate === root || candidate.startsWith(`${root}${sep}`);
}

async function hashFile(path: string): Promise<string> {
  const digest = createHash("sha256");
  await new Promise<void>((resolvePromise, reject) => {
    const stream = createReadStream(path);
    stream.on("data", (chunk) => digest.update(chunk));
    stream.on("error", reject);
    stream.on("end", resolvePromise);
  });
  return digest.digest("hex");
}
