// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. runtime-secrets module for scripts/src/artifact-registry.ts.

// Moved mechanically from scripts/src/artifact-registry.ts:2232-2390 for #903.

import { createHash } from "node:crypto";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { runCommand, type CommandResult } from "../command";
import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "../config";
import { startJob } from "../jobs";
import {
  compareDeployJsonExecutorMirrors,
  deployJsonCommitImage,
  deployJsonDriftResult,
  deployJsonSourceOfTruth,
  hasDeployJsonExecutorContract,
  k3sManifestExecutorMirror,
  parseDeployJsonServiceContractBase64,
  readDeployJsonServiceContractFromFile,
  type DeployJsonExecutorMirror,
  type DeployJsonServiceContract,
} from "../deploy-json-contract";
import { d601K3sGuardShellLines } from "../d601-k3s-guard";
import { composeRuntimeEnvValue } from "../runtime-env";

import type { ArtifactConsumerTarget, ArtifactDeployEnvironment, ArtifactRegistryOptions, AuthHealthGate, ComposeArtifactRuntime, RuntimeSecretContract, RuntimeSecretPresence, RuntimeSecretRequirement, RuntimeSecretSource } from "./types";
import { composeFile, safeName, shellQuote } from "./bundle";
import { baiduNetdiskAuthHealthGate } from "./catalog";
import { runtimeSecretPresenceFromEnvText } from "./install";
import { combineCommandResults, downloadRemoteFile, runRemoteScript, runRemoteScriptBackground } from "./remote";

export function baiduNetdiskAuthHealthGateStatus(health: unknown): { ok: boolean; requiredFields: string[]; failedFields: string[] } {
  const record = typeof health === "object" && health !== null && !Array.isArray(health) ? health as Record<string, unknown> : {};
  const auth = typeof record.auth === "object" && record.auth !== null && !Array.isArray(record.auth) ? record.auth as Record<string, unknown> : {};
  const failedFields = baiduNetdiskAuthHealthGate.requiredFields.filter((field) => auth[field] !== true);
  return {
    ok: failedFields.length === 0,
    requiredFields: baiduNetdiskAuthHealthGate.requiredFields,
    failedFields,
  };
}

export function composeArtifactRuntime(config: UniDeskConfig, target: ArtifactConsumerTarget): ComposeArtifactRuntime {
  if (target.compose === undefined) throw new Error("compose artifact target missing compose config");
  const compose = target.compose;
  const workDir = compose.workDir ?? config.providerGateway.upgrade.hostProjectRoot;
  const composeFile = compose.composeFile ?? config.providerGateway.upgrade.composeFile;
  const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
  const envFile = join(workDir, composeEnvFile);
  const project = compose.projectHint ?? (config.providerGateway.upgrade.composeProject || config.docker.projectName);
  const command = ["docker", "compose", "--env-file", envFile, "-f", join(workDir, composeFile), "-p", project];
  return { workDir, composeFile, composeEnvFile, envFile, project, command };
}

export function runtimeSecretSourceForTarget(config: UniDeskConfig, target: ArtifactConsumerTarget): RuntimeSecretSource {
  const runtime = composeArtifactRuntime(config, target);
  return {
    kind: "compose-env-file",
    path: runtime.envFile,
    exists: existsSync(runtime.envFile),
    configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
    workDir: runtime.workDir,
    composeEnvFile: runtime.composeEnvFile,
    composeService: target.compose?.serviceName ?? null,
    containerName: target.compose?.containerName ?? null,
    valuesPrinted: false,
  };
}

export function runtimeSecretRecommendedAction(missing: string[]): string {
  if (missing.length === 0) return "none";
  return "Restore the missing source env keys in the canonical Compose env file without printing values, then rerun deploy apply --env <env> --service <service> --dry-run before any live apply.";
}

export function runtimeSecretRecommendedActionForService(contract: RuntimeSecretContract, environment: ArtifactDeployEnvironment, serviceId: string): RuntimeSecretContract {
  if (contract.requiredSecretsPresent) return { ...contract, recommendedAction: "none" };
  return {
    ...contract,
    recommendedAction: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${serviceId} --dry-run before any live apply.`,
  };
}

export function runtimeSecretContractFromPresence(
  source: RuntimeSecretSource,
  requirements: RuntimeSecretRequirement[],
  presence: RuntimeSecretPresence[],
): RuntimeSecretContract {
  const missingSecretKeys = presence.filter((item) => !item.present).map((item) => item.sourceEnvName);
  const requiredSecretsPresent = requirements.length === 0 || (source.exists && missingSecretKeys.length === 0);
  return {
    check: "runtime-secret-presence",
    secretSource: source,
    requiredSecretsPresent,
    missingSecretKeys,
    recommendedAction: runtimeSecretRecommendedAction(missingSecretKeys),
    valuesPrinted: false,
    requirements: presence.map((item) => ({
      sourceEnvName: item.sourceEnvName,
      containerEnvName: item.containerEnvName,
      present: item.present,
      valuePrinted: false,
    })),
    dryRunDisposition: requirements.length === 0
      ? "not-required"
      : requiredSecretsPresent
        ? "ready-for-live-apply"
        : "secret-source-blocked",
  };
}

export function runtimeSecretContractFromEnvText(
  envText: string,
  requirements: RuntimeSecretRequirement[],
  sourceOverrides: Partial<Omit<RuntimeSecretSource, "kind" | "valuesPrinted">> = {},
): RuntimeSecretContract {
  const source: RuntimeSecretSource = {
    kind: "compose-env-file",
    path: sourceOverrides.path ?? "<env-text>",
    exists: sourceOverrides.exists ?? true,
    configSource: sourceOverrides.configSource ?? "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
    workDir: sourceOverrides.workDir ?? "",
    composeEnvFile: sourceOverrides.composeEnvFile ?? "",
    composeService: sourceOverrides.composeService ?? null,
    containerName: sourceOverrides.containerName ?? null,
    valuesPrinted: false,
  };
  return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
}

export function runtimeSecretContractForComposeTarget(
  config: UniDeskConfig,
  target: ArtifactConsumerTarget,
  requirements: RuntimeSecretRequirement[] | undefined,
): RuntimeSecretContract | undefined {
  if (requirements === undefined) return undefined;
  const source = runtimeSecretSourceForTarget(config, target);
  const envText = source.exists ? readFileSync(source.path, "utf8") : "";
  return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
}

export function composeArtifactSecretPreflight(envFile: string, requirements: RuntimeSecretRequirement[] | undefined): { ok: boolean; envFile: string; requirements: RuntimeSecretPresence[]; missing: RuntimeSecretPresence[]; valuesLogged: false } {
  const requirementList = requirements ?? [];
  const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
  const presence = runtimeSecretPresenceFromEnvText(envText, requirementList);
  const missing = presence.filter((item) => !item.present);
  return {
    ok: requirementList.length === 0 || (existsSync(envFile) && missing.length === 0),
    envFile,
    requirements: presence,
    missing,
    valuesLogged: false,
  };
}

export function authHealthGateScript(gate: AuthHealthGate | undefined, healthJsonShellArg: string): string[] {
  if (gate === undefined) return [];
  return [
    `auth_health_gate_fields=${shellQuote(gate.requiredFields.join(","))}`,
    `python3 - ${healthJsonShellArg} "$auth_health_gate_fields" <<'PY'`,
    "import json, sys",
    "path, fields_text = sys.argv[1:]",
    "fields = [item for item in fields_text.split(',') if item]",
    "body = json.load(open(path, encoding='utf-8'))",
    "auth = body.get('auth') if isinstance(body, dict) else None",
    "auth = auth if isinstance(auth, dict) else {}",
    "failed = [field for field in fields if auth.get(field) is not True]",
    "if failed:",
    "    print('baidu_netdisk_auth_health_gate_failed=' + ','.join(failed))",
    "    raise SystemExit(1)",
    "print('baidu_netdisk_auth_health_gate=ok')",
    "PY",
  ];
}

export function upsertEnvFileValues(path: string, values: Record<string, string>): void {
  const existing = readFileSync(path, "utf8");
  const seen = new Set<string>();
  const lines = existing.split(/\n/u).filter((line, index, array) => index < array.length - 1 || line.length > 0).map((line) => {
    const match = /^([A-Za-z0-9_]+)=/u.exec(line);
    if (match === null || values[match[1]] === undefined) return line;
    seen.add(match[1]);
    return `${match[1]}=${values[match[1]]}`;
  });
  for (const [key, value] of Object.entries(values)) {
    if (!seen.has(key)) lines.push(`${key}=${value}`);
  }
  writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
}

export async function pullArtifactFromD601(options: ArtifactRegistryOptions, sourceImage: string): Promise<CommandResult> {
  const localArchive = `/tmp/unidesk-artifact-${safeName(options.serviceId ?? "service")}-${safeName(options.commit ?? "commit")}-${Date.now().toString(36)}.tar.gz`;
  const remoteArchive = `/tmp/unidesk-artifact-${safeName(options.serviceId ?? "service")}-${safeName(options.commit ?? "commit")}-${Date.now().toString(36)}.tar.gz`;
  const remoteScript = [
    "set -euo pipefail",
    `image=${shellQuote(sourceImage)}`,
    `archive=${shellQuote(remoteArchive)}`,
    "rm -f \"$archive\"",
    "export DOCKER_CONFIG=$(mktemp -d /tmp/unidesk-artifact-docker-config.XXXXXX)",
    "trap 'rm -rf \"$DOCKER_CONFIG\"' EXIT",
    "printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
    "docker pull -q \"$image\" >/dev/null",
    "docker image inspect \"$image\" --format 'remote_source={{ index .Config.Labels \"unidesk.ai/source-commit\" }} remote_service={{ index .Config.Labels \"unidesk.ai/service-id\" }} remote_dockerfile={{ index .Config.Labels \"unidesk.ai/dockerfile\" }}' >&2",
    "docker save \"$image\" | gzip -1 > \"$archive\"",
    "remote_archive_bytes=$(wc -c < \"$archive\" | tr -d '[:space:]')",
    "remote_archive_sha256=$(sha256sum \"$archive\" | awk '{print $1}')",
    "printf 'remote_archive=%s\\nremote_archive_bytes=%s\\nremote_archive_sha256=%s\\n' \"$archive\" \"$remote_archive_bytes\" \"$remote_archive_sha256\"",
  ].join("\n");
  const archive = await runRemoteScriptBackground(options, remoteScript, Math.max(options.timeoutMs, 900_000), "docker-save");
  if (archive.exitCode !== 0 || archive.timedOut) return archive;
  const download = downloadRemoteFile(options, remoteArchive, localArchive, Math.max(options.timeoutMs, 900_000));
  if (download.exitCode !== 0 || download.timedOut) {
    runRemoteScript(options, `rm -f ${shellQuote(remoteArchive)}`, 30_000);
    try {
      rmSync(localArchive, { force: true });
    } catch {
      // Best-effort cleanup only.
    }
    return combineCommandResults(["artifact-registry", "pull-artifact-from-d601"], [archive, download]);
  }
  const load = runCommand(["bash", "-lc", `set -euo pipefail; gzip -dc ${shellQuote(localArchive)} | docker load`], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 900_000) });
  runRemoteScript(options, `rm -f ${shellQuote(remoteArchive)}`, 30_000);
  try {
    rmSync(localArchive, { force: true });
  } catch {
    // Best-effort cleanup only.
  }
  return combineCommandResults(["artifact-registry", "pull-artifact-from-d601"], [archive, download, load]);
}
