// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. status module for scripts/src/artifact-registry.ts.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.

// Moved mechanically from scripts/src/artifact-registry.ts:1081-1318 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 { k3sGuardShellLines } from "../k3s-target-guard";
import { composeRuntimeEnvValue } from "../runtime-env";

import { artifactRegistryOptionsTargetSummary, type ArtifactRegistryFailureClassification, type ArtifactRegistryOptions, type RenderedBundle } from "./types";
import { renderBundle, shellQuote } from "./bundle";

export function plan(options: ArtifactRegistryOptions): Record<string, unknown> {
  const bundle = renderBundle(options);
  return {
    ok: true,
    target: artifactRegistryOptionsTargetSummary(options),
    providerId: options.providerId,
    mode: "d601-host-managed",
    firstStage: true,
    runtime: {
      implementation: "CNCF Distribution Docker registry",
      image: options.image,
      endpoint: `http://${options.host}:${options.port}`,
      publicExposure: "none",
      orchestration: "systemd + Docker Compose on D601 host/WSL OS",
      k3sManaged: false,
    },
    dependencies: [
      { name: "Docker Engine", scope: "D601 host", requiredFor: "running the registry container" },
      { name: "Docker Compose plugin", scope: "D601 host", requiredFor: "systemd ExecStart/ExecStop" },
      { name: "systemd", scope: "D601 host", requiredFor: "host-managed service lifecycle" },
      { name: "registry:2.8.3", scope: "D601 Docker cache / upstream pull", requiredFor: "CNCF Distribution runtime image" },
      { name: "provider-gateway Host SSH", scope: "UniDesk control bridge", requiredFor: "readonly status and health checks" },
      { name: "local filesystem storage", scope: bundle.paths.storage, requiredFor: "artifact persistence outside k3s" },
    ],
    boundaries: [
      "listen only on D601 host loopback 127.0.0.1:5000",
      "do not expose a public port, NodePort, hostPort, or third-party registry",
      "do not run inside k3s; keep the registry outside the native k3s failure domain",
      "CI builds and publishes backend-core, frontend, and reviewed user-service artifacts on D601",
      "CD only pulls, retags, recreates/imports artifacts, and verifies live commit",
      "master server CD must not compile Rust or run docker compose build for artifact consumers",
      "code-queue is dev-only for artifact consumer validation and has no prod artifact deploy target",
    ],
    renderedPaths: bundle.paths,
    artifactConsumerFlow: [
      "D601 CI builds unidesk/<service-id>:<commit>",
      "D601 CI pushes 127.0.0.1:5000/unidesk/<service-id>:<commit>",
      "Compose consumers pull via a controlled provider-gateway SSH image stream",
      "k3s consumers pull on D601 and import into native k3s containerd",
      "CD retags/recreates or updates Deployment images and verifies live commit metadata",
    ],
    frontendArtifactFlow: [
      "D601 CI builds and pushes 127.0.0.1:5000/unidesk/frontend:<commit>",
      "dev CD imports the image into D601 native k3s and verifies frontend-dev /health deploy.commit",
      "prod CD pulls via provider-gateway image stream, recreates Compose frontend with no build, and verifies /health deploy.commit",
    ],
  };
}

export function statusScript(options: ArtifactRegistryOptions, bundle: RenderedBundle): string {
  const hashes = Object.fromEntries(bundle.files.map((item) => [item.path, item.sha256]));
  return `set -u
kv() { printf '%s=%s\\n' "$1" "$2"; }
bool_file() { [ -f "$1" ] && printf true || printf false; }
bool_dir() { [ -d "$1" ] && printf true || printf false; }
hash_file() { sha256sum "$1" 2>/dev/null | awk '{print $1}' || true; }
unit=${shellQuote(bundle.paths.unit)}
compose=${shellQuote(bundle.paths.compose)}
config=${shellQuote(bundle.paths.config)}
storage=${shellQuote(bundle.paths.storage)}
container=${shellQuote(options.containerName)}
expected_image=${shellQuote(options.image)}
port=${options.port}
kv readonly true
kv unit_exists "$(bool_file "$unit")"
kv compose_exists "$(bool_file "$compose")"
kv config_exists "$(bool_file "$config")"
kv storage_exists "$(bool_dir "$storage")"
if command -v systemctl >/dev/null 2>&1; then
  kv systemctl_available true
  kv unit_active "$(systemctl is-active "${options.unitName}" 2>/dev/null || true)"
  kv unit_enabled "$(systemctl is-enabled "${options.unitName}" 2>/dev/null || true)"
else
  kv systemctl_available false
  kv unit_active unknown
  kv unit_enabled unknown
fi
if command -v docker >/dev/null 2>&1; then
  kv docker_available true
  container_running="$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null || true)"
  container_status="$(docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || true)"
  container_image="$(docker inspect -f '{{.Config.Image}}' "$container" 2>/dev/null || true)"
  container_restart_policy="$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$container" 2>/dev/null || true)"
  kv container_running "$([ -n "$container_running" ] && printf '%s' "$container_running" || printf false)"
  kv container_status "$container_status"
  kv container_image "$container_image"
  kv container_restart_policy "$container_restart_policy"
else
  kv docker_available false
  kv container_running false
  kv container_status unknown
  kv container_image ""
  kv container_restart_policy unknown
fi
if command -v ss >/dev/null 2>&1; then
  listeners="$(ss -ltnH 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" {print $4}' || true)"
else
  listeners=""
fi
listener_count="$(printf '%s\\n' "$listeners" | sed '/^$/d' | wc -l | tr -d ' ')"
bad_listener_count="$(printf '%s\\n' "$listeners" | sed '/^$/d' | grep -Ev "^(127[.]0[.]0[.]1|localhost):${options.port}$|^\\[::1\\]:${options.port}$|^::1:${options.port}$" | wc -l | tr -d ' ')"
loopback_only=false
if [ "\${listener_count:-0}" -gt 0 ] && [ "\${bad_listener_count:-0}" -eq 0 ]; then loopback_only=true; fi
kv listener_count "\${listener_count:-0}"
kv bad_listener_count "\${bad_listener_count:-0}"
kv loopback_only "$loopback_only"
if command -v curl >/dev/null 2>&1; then
  kv curl_available true
  kv v2_http_code "$(timeout 3 curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:${options.port}/v2/ 2>/dev/null || true)"
else
  kv curl_available false
  kv v2_http_code ""
fi
config_hash="$(hash_file "$config")"
compose_hash="$(hash_file "$compose")"
unit_hash="$(hash_file "$unit")"
kv config_hash_matches "$([ -n "$config_hash" ] && [ "$config_hash" = ${shellQuote(hashes[bundle.paths.config] ?? "")} ] && printf true || printf false)"
kv compose_hash_matches "$([ -n "$compose_hash" ] && [ "$compose_hash" = ${shellQuote(hashes[bundle.paths.compose] ?? "")} ] && printf true || printf false)"
kv unit_hash_matches "$([ -n "$unit_hash" ] && [ "$unit_hash" = ${shellQuote(hashes[bundle.paths.unit] ?? "")} ] && printf true || printf false)"
kv image_matches "$([ "\${container_image:-}" = "$expected_image" ] && printf true || printf false)"
kv config_hash "$config_hash"
kv compose_hash "$compose_hash"
kv unit_hash "$unit_hash"
`;
}

export function parseKeyValueOutput(stdout: string): Record<string, string> {
  const values: Record<string, string> = {};
  for (const line of stdout.split(/\r?\n/u)) {
    const match = /^([A-Za-z0-9_]+)=(.*)$/u.exec(line);
    if (match !== null) values[match[1]] = match[2];
  }
  return values;
}

export function asBool(value: string | undefined): boolean {
  return value === "true";
}

export function registryRecommendedAction(classification: ArtifactRegistryFailureClassification | null): string {
  if (classification === null) return "none";
  if (classification === "control-plane-missing") return "restore the artifact registry readonly control plane, or rerun through --main-server-ip <host> once frontend/backend-core dispatch is reachable";
  if (classification === "local-docker-required") return "run the read-only check through --main-server-ip <host> or from a main-server CLI with backend-core available";
  if (classification === "provider-ssh-command-missing") return "restore D601 provider-gateway host.ssh dispatch/capability before retrying artifact registry health";
  if (classification === "ssh-helper-command-shape-incompatible") return "upgrade the CLI/control-plane host.ssh helper shape so it can run bash -lc readonly probes";
  if (classification === "remote-command-timeout") return "rerun artifact-registry health and inspect D601 host.ssh latency if the timeout repeats";
  if (classification === "registry-not-installed") return "install the D601 artifact registry through the controlled artifact-registry install path, then rerun health";
  return "restore the D601 artifact registry service/container/API until registry health passes";
}

export function readonlyRemoteCommandShape(action: "status" | "health", options: ArtifactRegistryOptions): string {
  if (isLocalProvider(options.providerId)) return `local bash -lc <artifact-registry-${action}-readonly-script> timeoutMs=${options.timeoutMs}`;
  return `host.ssh provider=${options.providerId} mode=exec argv=bash -lc <artifact-registry-${action}-readonly-script> timeoutMs=${options.timeoutMs}`;
}

export function isLocalProvider(providerId: string): boolean {
  return providerId === "local" || providerId === "D601-local";
}

export function classifyProviderSshCommandFailure(command: CommandResult): ArtifactRegistryFailureClassification {
  const output = `${command.stderr}\n${command.stdout}`.toLowerCase();
  if (command.timedOut || output.includes("timed out") || output.includes("timeout")) return "remote-command-timeout";
  if (output.includes("no such container: unidesk-backend-core") || output.includes("no such container: unidesk-database") || output.includes("backend-core bridge unavailable")) {
    return "local-docker-required";
  }
  if (output.includes("provider does not declare host.ssh") || output.includes("provider is offline") || output.includes("host.ssh capability")) {
    return "provider-ssh-command-missing";
  }
  if (output.includes("remote frontend transport does not support") || output.includes("ssh helper") || output.includes("unsupported command") || output.includes("bad substitution") || output.includes("command is too long")) {
    return "ssh-helper-command-shape-incompatible";
  }
  if (command.exitCode === 126 || command.exitCode === 127 || output.includes("command not found") || output.includes("exec format error")) {
    return "ssh-helper-command-shape-incompatible";
  }
  return "provider-ssh-command-missing";
}

export function providerSshCommandFailureScopes(classification: ArtifactRegistryFailureClassification): string[] {
  const scopes = ["provider-ssh-command"];
  if (classification === "control-plane-missing") return ["control-plane-missing", ...scopes];
  if (classification === "remote-command-timeout") scopes.push("remote-command-timeout");
  if (classification === "local-docker-required") scopes.push("local-docker-control-plane");
  if (classification === "ssh-helper-command-shape-incompatible") scopes.push("ssh-helper-command-shape");
  return scopes;
}

export function registryHealthFailureClassification(checks: Record<string, boolean>, failedScopes: string[]): ArtifactRegistryFailureClassification | null {
  if (failedScopes.length === 0) return null;
  const installed = checks.unitExists === true
    || checks.composeExists === true
    || checks.configExists === true
    || checks.storageExists === true
    || checks.containerRunning === true;
  return installed ? "registry-unhealthy" : "registry-not-installed";
}

export function registryHealthDecision(checks: Record<string, boolean>, commandOk: boolean): Record<string, unknown> {
  const scopeChecks: Array<[string, boolean]> = [
    ["systemd", checks.systemctlAvailable === true && checks.unitExists === true && checks.unitActive === true],
    ["docker", checks.dockerAvailable === true],
    ["registry-container", checks.containerRunning === true],
    ["loopback-listener", checks.loopbackOnly === true],
    ["registry-api", checks.v2Ok === true],
    ["storage", checks.storageExists === true],
    ["rendered-config", checks.configHashMatches === true && checks.composeHashMatches === true && checks.unitHashMatches === true],
    ["registry-image", checks.imageMatches === true],
  ];
  const healthyScopes = scopeChecks.filter(([, ok]) => ok).map(([scope]) => scope);
  const failedScopes = scopeChecks.filter(([, ok]) => !ok).map(([scope]) => scope);
  const runtimeApiHealthy = checks.containerRunning === true && checks.loopbackOnly === true && checks.v2Ok === true;
  const failureClassification = commandOk ? registryHealthFailureClassification(checks, failedScopes) : "provider-ssh-command-missing";
  return {
    decision: commandOk && failedScopes.length === 0 ? "healthy" : commandOk || runtimeApiHealthy ? "service-degraded" : "retryable-transient",
    retryable: true,
    failureClassification,
    healthyScopes,
    failedScopes,
    recommendedAction: registryRecommendedAction(failureClassification),
    runtimeApiHealthy,
  };
}
