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

// Moved mechanically from scripts/src/deploy.ts:536-633 for #903.

import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { runCommand } from "../command";
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "../config";
import { ensureGithubSshIdentityForProvider } from "../deploy-ssh-identity";
import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "../artifact-registry";
import { startJob } from "../jobs";
import { coreInternalFetch } from "../microservices";
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "../code-queue-source-guard";
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard";
import { composeRuntimeEnvValue } from "../runtime-env";
import {
  compareDeployJsonExecutorMirrors,
  deployJsonCommitImage,
  deployJsonDriftResult,
  deployJsonSourceOfTruth,
  encodeDeployJsonServiceContract,
  hasDeployJsonExecutorContract,
  k3sManifestExecutorMirror,
  parseDeployJsonServiceContract,
  type DeployJsonExecutorMirror,
  type DeployJsonServiceContract,
} from "../deploy-json-contract";

import type { DeployEnvironment, DeployEnvironmentManifestSource, DeployManifest, DeployManifestService } from "./types";
import { asRecord, commandFailure, isDeployEnvironment, nowIso, parseFullCommit, runGitOrThrow } from "./options";
import { deployEnvironmentTargets } from "./types";

export function parseDeployManifestService(item: unknown, index: number, seen: Set<string>, path: string): DeployManifestService {
  const service = parseDeployJsonServiceContract(item, `deploy manifest ${path}[${index}]`);
  const id = service.id;
  const repo = service.repo;
  const commitId = service.commitId;
  if (id.length === 0) throw new Error(`deploy manifest ${path}[${index}].id must be a non-empty string`);
  if (repo.length === 0) throw new Error(`deploy manifest ${path}[${index}].repo must be a non-empty string`);
  if (!/^[0-9a-f]{7,40}$/iu.test(commitId)) throw new Error(`deploy manifest ${path}[${index}].commitId must be a 7-40 char git SHA`);
  if (seen.has(id)) throw new Error(`duplicate deploy manifest service id: ${id}`);
  seen.add(id);
  return service;
}

export function parseDeployManifestServices(value: unknown, path: string): DeployManifestService[] {
  if (!Array.isArray(value)) throw new Error(`deploy manifest ${path} must be an array`);
  const seen = new Set<string>();
  return value.map((item, index) => parseDeployManifestService(item, index, seen, path));
}

export function parseDeployManifest(parsed: unknown, source: string, expectedEnvironment: DeployEnvironment | null): DeployManifest {
  const record = asRecord(parsed);
  if (record === null) throw new Error(`deploy manifest ${source} must be an object`);
  if (record.schemaVersion !== 1 && record.schemaVersion !== 2) throw new Error("deploy manifest schemaVersion must be 1 or 2");
  if (record.schemaVersion === 2) {
    const environments = asRecord(record.environments);
    if (environments === null) throw new Error("deploy manifest schemaVersion=2 requires environments");
    if (expectedEnvironment === null) {
      const prod = asRecord(environments.prod);
      if (prod === null) throw new Error("deploy manifest schemaVersion=2 requires environments.prod for local compatibility mode");
      return { schemaVersion: 2, environment: "prod", services: parseDeployManifestServices(prod.services, "environments.prod.services") };
    }
    const envRecord = asRecord(environments[expectedEnvironment]);
    if (envRecord === null) throw new Error(`deploy manifest ${source} does not contain environments.${expectedEnvironment}`);
    return { schemaVersion: 2, environment: expectedEnvironment, services: parseDeployManifestServices(envRecord.services, `environments.${expectedEnvironment}.services`) };
  }
  const rawEnvironment = record.environment;
  let environment: DeployEnvironment | null = null;
  if (rawEnvironment !== undefined) {
    if (typeof rawEnvironment !== "string" || !isDeployEnvironment(rawEnvironment)) {
      throw new Error("deploy manifest environment must be dev or prod when present");
    }
    environment = rawEnvironment;
  }
  if (expectedEnvironment !== null) {
    if (environment === null) throw new Error(`deploy manifest ${source} must declare environment=${expectedEnvironment} when --env ${expectedEnvironment} is used`);
    if (environment !== expectedEnvironment) {
      throw new Error(`deploy manifest ${source} declares environment=${environment}, refusing --env ${expectedEnvironment}`);
    }
  }
  return { schemaVersion: 1, environment, services: parseDeployManifestServices(record.services, "services") };
}

export function readEnvironmentDeployManifest(environment: DeployEnvironment): { manifest: DeployManifest; source: DeployEnvironmentManifestSource } {
  const target = deployEnvironmentTargets[environment];
  const remoteRef = `refs/remotes/${target.remote}/${target.branch}`;
  const fetch = runCommand(["git", "fetch", "--no-tags", target.remote, `+refs/heads/${target.branch}:${remoteRef}`], repoRoot, { timeoutMs: 60_000 });
  if (fetch.exitCode !== 0) {
    throw new Error(`failed to fetch ${target.gitRef} for deploy --env ${environment}: ${commandFailure(fetch)}`);
  }
  const commit = parseFullCommit(runGitOrThrow(["rev-parse", "--verify", `${target.gitRef}^{commit}`], repoRoot, `failed to resolve ${target.gitRef}`).stdout);
  if (commit.length !== 40) throw new Error(`failed to resolve a full commit for ${target.gitRef}`);
  const blob = runGitOrThrow(["rev-parse", `${target.gitRef}:deploy.json`], repoRoot, `failed to resolve ${target.gitRef}:deploy.json`).stdout.trim().toLowerCase();
  if (!/^[0-9a-f]{40}$/iu.test(blob)) throw new Error(`failed to resolve a deploy.json blob for ${target.gitRef}`);
  const raw = runGitOrThrow(["show", `${target.gitRef}:deploy.json`], repoRoot, `failed to read ${target.gitRef}:deploy.json`).stdout;
  const manifest = parseDeployManifest(JSON.parse(raw) as unknown, `${target.gitRef}:deploy.json#environments.${environment}`, environment);
  return {
    manifest,
    source: {
      source: "git-ref",
      path: "deploy.json",
      ref: target.gitRef,
      remote: target.remote,
      branch: target.branch,
      commit,
      blob,
      fetchedAt: nowIso(),
    },
  };
}

export function readProdDeployManifestSnapshot(): DeployManifest {
  const raw = runGitOrThrow(["show", `${deployEnvironmentTargets.prod.gitRef}:deploy.json`], repoRoot, `failed to read ${deployEnvironmentTargets.prod.gitRef}:deploy.json`).stdout;
  return parseDeployManifest(JSON.parse(raw) as unknown, `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`, "prod");
}

export async function readDeployManifest(file: string): Promise<DeployManifest> {
  const path = resolve(repoRoot, file);
  if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`);
  if (/\.(?:mjs|js|cjs)$/iu.test(path)) {
    const moduleValue = await import(`${pathToFileURL(path).href}?unideskDeployManifest=${Date.now()}`);
    const moduleRecord = asRecord(moduleValue);
    const parsed = moduleRecord?.default ?? moduleRecord?.manifest;
    if (parsed === undefined) throw new Error(`deploy JS manifest must export default or named manifest: ${path}`);
    return parseDeployManifest(parsed, path, null);
  }
  return parseDeployManifest(JSON.parse(readFileSync(path, "utf8")) as unknown, path, null);
}
