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

// Moved mechanically from scripts/src/deploy.ts:1237-1518 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 { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-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, DeployManifestService, DeployOptions } from "./types";
import { isUnideskRepo, safeId, shellQuote } from "./options";
import { writeComposeEnvFallbackPath } from "./scripts";
import { coreDeployService, devK3sDeployService, isDevK3sDeployService } from "./service-plan";
import { k3sProductionHostPathRepoDir, providerGatewayWsEgressProxyUrl, remoteDeployRoot, unideskRepoUrl } from "./types";

export function environmentPlanServiceConfig(config: UniDeskConfig | null, service: DeployManifestService, environment: DeployEnvironment): UniDeskMicroserviceConfig | null {
  if (environment === "dev") {
    const devService = devK3sDeployService(service.id);
    if (devService !== undefined) return devService;
  }
  const configured = config?.microservices.find((candidate) => candidate.id === service.id);
  if (configured !== undefined) return configured;
  if (config !== null) return coreDeployService(config, service.id, environment) ?? null;
  return null;
}

export function targetIsMain(service: UniDeskMicroserviceConfig): boolean {
  return service.providerId === "main-server";
}

export function targetDeployRoot(service: UniDeskMicroserviceConfig): string {
  return targetIsMain(service) ? rootPath(".state", "deploy") : remoteDeployRoot;
}

export function targetRepoDir(service: UniDeskMicroserviceConfig): string {
  return `${targetDeployRoot(service)}/repos/${safeId(service.id)}`;
}

export function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): string {
  return `${targetDeployRoot(service)}/exports/${safeId(service.id)}-${runId}`;
}

export function targetWorkDir(service: UniDeskMicroserviceConfig): string {
  if (isDevK3sDeployService(service)) return service.development.worktreePath;
  if (service.deployment.mode === "k3sctl-managed") return k3sProductionHostPathRepoDir;
  if (targetIsMain(service) && isUnideskRepo(service.repository.url)) {
    return rootPath(".state", "deploy", "work", safeId(service.id));
  }
  return service.development.worktreePath;
}

export function sourceWorkDir(service: UniDeskMicroserviceConfig): string {
  if (service.deployment.mode === "k3sctl-managed" && service.repository.url !== unideskRepoUrl) {
    return `${remoteDeployRoot}/work/${safeId(service.id)}`;
  }
  return targetWorkDir(service);
}

export function sourceFallbackWorktree(service: UniDeskMicroserviceConfig): string {
  if (targetIsMain(service) || isUnideskRepo(service.repository.url)) return "";
  return service.development.worktreePath;
}

export function sourceRootSubdir(service: UniDeskMicroserviceConfig): string {
  const claudeqqPrefix = "claudeqq/";
  if (service.id === "claudeqq" && service.repository.url !== unideskRepoUrl && service.repository.dockerfile.startsWith(claudeqqPrefix)) {
    return "claudeqq";
  }
  return "";
}

export function sourceBuildContext(service: UniDeskMicroserviceConfig): string {
  const subdir = sourceRootSubdir(service);
  return subdir.length > 0 ? `${sourceWorkDir(service)}/${subdir}` : sourceWorkDir(service);
}

export function buildImageTag(service: UniDeskMicroserviceConfig): string {
  if (isDevK3sDeployService(service)) return `unidesk-${service.id}:dev`;
  if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:d601`;
  if (targetIsMain(service)) {
    if (["project-manager", "baidu-netdisk", "oa-event-flow"].includes(service.repository.composeService)) return service.repository.composeService;
    return `unidesk-${service.repository.composeService}`;
  }
  return `unidesk-${service.id}:${service.providerId.toLowerCase()}`;
}

export function codeQueueContainerdImagePreflight(imageListText: string, expectedImage: string): { ok: boolean; expectedImage: string; matchedLine: string | null; error: string | null } {
  const matchedLine = imageListText
    .split(/\r?\n/u)
    .map((line) => line.trim())
    .find((line) => line.length > 0 && line.includes(expectedImage)) ?? null;
  return matchedLine === null
    ? {
      ok: false,
      expectedImage,
      matchedLine: null,
      error: `native k3s containerd is missing required image tag: ${expectedImage}`,
    }
    : { ok: true, expectedImage, matchedLine, error: null };
}

export function codeQueueManifestImagePreflight(manifestText: string, expectedImage: string): { ok: boolean; expectedImage: string; objects: Array<{ kind: string; name: string; images: string[]; ok: boolean }>; errors: string[] } {
  const requiredObjectNames = new Set(["code-queue", "code-queue-read", "code-queue-write", "d601-provider-egress-proxy", "d601-tcp-egress-gateway"]);
  const objects: Array<{ kind: string; name: string; images: string[]; ok: boolean }> = [];
  for (const documentText of manifestText.split(/^---\s*$/mu)) {
    const kind = documentText.match(/^kind:\s*(\S+)\s*$/mu)?.[1] ?? "";
    if (kind !== "Deployment") continue;
    const deploymentName = documentText.match(/^metadata:\s*$[\s\S]*?^\s{2}name:\s*([A-Za-z0-9_.-]+)\s*$/mu)?.[1] ?? "";
    if (!requiredObjectNames.has(deploymentName)) continue;
    const images = Array.from(documentText.matchAll(/^\s*image:\s*(?:"([^"]+)"|([^\s#]+))/gmu)).map((match) => match[1] ?? match[2] ?? "");
    objects.push({ kind, name: deploymentName, images, ok: images.length > 0 && images.every((image) => image === expectedImage) });
  }
  const presentNames = new Set(objects.map((object) => object.name));
  const errors = [
    ...Array.from(requiredObjectNames).filter((name) => !presentNames.has(name)).map((name) => `required deployment missing from manifest: ${name}`),
    ...objects.filter((object) => !object.ok).map((object) => `deployment ${object.name} must use only ${expectedImage}; found ${object.images.join(",") || "<none>"}`),
  ];
  return { ok: errors.length === 0, expectedImage, objects, errors };
}

export function directComposeFile(service: UniDeskMicroserviceConfig): string {
  return targetIsMain(service)
    ? rootPath("docker-compose.yml")
    : `${targetWorkDir(service)}/${service.repository.composeFile}`;
}

export function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
  return targetIsMain(service) ? writeComposeEnvFallbackPath() : "";
}

export function redactedSecretContractForService(config: UniDeskConfig | null, service: UniDeskMicroserviceConfig, environment: DeployEnvironment): RuntimeSecretContract | undefined {
  if (config === null || service.id !== "baidu-netdisk" || !targetIsMain(service)) return undefined;
  const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
  const envFile = join(config.providerGateway.upgrade.hostProjectRoot, composeEnvFile);
  const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
  const contract = runtimeSecretContractFromEnvText(envText, baiduNetdiskRuntimeSecretRequirements, {
    path: envFile,
    exists: existsSync(envFile),
    workDir: config.providerGateway.upgrade.hostProjectRoot,
    composeEnvFile,
    composeService: service.repository.composeService,
    containerName: service.repository.containerName,
  });
  return {
    ...contract,
    recommendedAction: contract.requiredSecretsPresent
      ? "none"
      : `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${service.id} --dry-run before any live apply.`,
  };
}

export function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
  if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return targetWorkDir(service);
  return "";
}

export function directDockerfileOverride(service: UniDeskMicroserviceConfig): string {
  if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return service.repository.dockerfile;
  return "";
}

export function k8sManifestPath(service: UniDeskMicroserviceConfig): string {
  const composeFile = service.repository.composeFile;
  if (composeFile.endsWith(".k8s.yaml")) return composeFile;
  if (!composeFile.endsWith(".k3s.json")) throw new Error(`${service.id} k3s service composeFile must point to *.k3s.json`);
  return composeFile.replace(/\.k3s\.json$/u, ".k8s.yaml");
}

export function sourceDockerfilePath(service: UniDeskMicroserviceConfig): string {
  const claudeqqPrefix = "claudeqq/";
  if (service.id === "claudeqq" && service.repository.url !== unideskRepoUrl && service.repository.dockerfile.startsWith(claudeqqPrefix)) {
    return service.repository.dockerfile.slice(claudeqqPrefix.length);
  }
  return service.repository.dockerfile;
}

export function sourceProxyPrelude(service: UniDeskMicroserviceConfig): string {
  if (targetIsMain(service)) return "";
  const strictHostKeyChecking = isUnideskRepo(service.repository.url) ? "yes" : "accept-new";
  return [
    "export DOCKER_CONFIG=/tmp/unidesk-docker-config-clean",
    "mkdir -p \"$DOCKER_CONFIG\"",
    "[ -f \"$DOCKER_CONFIG/config.json\" ] || printf '{}' > \"$DOCKER_CONFIG/config.json\"",
    `build_proxy=${shellQuote(providerGatewayWsEgressProxyUrl)}`,
    "export HTTP_PROXY=\"$build_proxy\" HTTPS_PROXY=\"$build_proxy\" ALL_PROXY=\"$build_proxy\"",
    "export NO_PROXY=\"localhost,127.0.0.1,::1,host.docker.internal\"",
    "git_ssh_proxy=/tmp/unidesk-git-ssh-http-connect.py",
    "cat > \"$git_ssh_proxy\" <<'UNIDESK_GIT_SSH_PROXY'",
    String.raw`#!/usr/bin/env python3
import os
import select
import socket
import sys
from urllib.parse import urlparse

if len(sys.argv) != 3:
    raise SystemExit("usage: unidesk-git-ssh-http-connect.py host port")

target_host = sys.argv[1]
target_port = int(sys.argv[2])
proxy = urlparse(os.environ.get("UNIDESK_GIT_SSH_HTTP_PROXY", "http://127.0.0.1:18789"))
proxy_host = proxy.hostname or "127.0.0.1"
proxy_port = proxy.port or 80

sock = socket.create_connection((proxy_host, proxy_port), timeout=20)
sock.sendall(f"CONNECT {target_host}:{target_port} HTTP/1.1\r\nHost: {target_host}:{target_port}\r\n\r\n".encode("ascii"))
header = b""
while b"\r\n\r\n" not in header:
    chunk = sock.recv(4096)
    if not chunk:
        raise SystemExit("proxy closed before CONNECT response")
    header += chunk
head, rest = header.split(b"\r\n\r\n", 1)
if not (head.startswith(b"HTTP/1.1 200") or head.startswith(b"HTTP/1.0 200")):
    sys.stderr.write(head.decode("latin1", "replace") + "\n")
    raise SystemExit(1)
if rest:
    os.write(1, rest)

stdin_open = True
sock.setblocking(False)
while True:
    readers = [sock]
    if stdin_open:
        readers.append(sys.stdin.buffer)
    ready, _, _ = select.select(readers, [], [])
    if sock in ready:
        try:
            data = sock.recv(65536)
        except BlockingIOError:
            data = b""
        if not data:
            break
        os.write(1, data)
    if stdin_open and sys.stdin.buffer in ready:
        data = os.read(0, 65536)
        if data:
            sock.sendall(data)
        else:
            stdin_open = False
            try:
                sock.shutdown(socket.SHUT_WR)
            except OSError:
                pass
`,
    "UNIDESK_GIT_SSH_PROXY",
    "chmod 700 \"$git_ssh_proxy\"",
    "export UNIDESK_GIT_SSH_HTTP_PROXY=\"$build_proxy\"",
    `export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=${strictHostKeyChecking} -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'"`,
    "curl -fsSI --max-time 20 -x \"$build_proxy\" https://github.com >/dev/null",
    "echo target_source_proxy=provider-gateway-ws-egress:$build_proxy",
    "echo target_build_proxy=provider-gateway-ws-egress:$build_proxy",
    "echo target_build_proxy_probe=ok",
  ].join("\n");
}

export function buildBaseImageFallbacks(service: UniDeskMicroserviceConfig): string[] {
  if (isDevK3sDeployService(service) && service.id === "code-queue") {
    return [
      "unidesk-code-queue:d601-build-base",
      "unidesk-code-queue:d601",
    ];
  }
  return [];
}

export function buildCachePrelude(dockerfileVariable: string, baseImageFallbacks: string[] = []): string[] {
  const fallbackLines = baseImageFallbacks.flatMap((image) => [
    `if [ "$base_image_found" = "0" ] && docker image inspect ${shellQuote(image)} >/dev/null 2>&1; then`,
    `  build_base_image=${shellQuote(image)}`,
    "  base_image_found=1",
    "fi",
  ]);
  return [
    "cache_args=(--cache-to type=inline --build-arg BUILDKIT_INLINE_CACHE=1)",
    "if docker image inspect \"$image\" >/dev/null 2>&1; then cache_args+=(--cache-from \"$image\"); echo target_build_cache_from_image=$image; else echo target_build_cache_from_image=missing:$image; fi",
    "echo target_build_cache_to=inline",
    "base_args=()",
    "build_base_image=\"${image}-build-base\"",
    "base_image_found=0",
    "if docker image inspect \"$build_base_image\" >/dev/null 2>&1; then base_image_found=1; fi",
    ...fallbackLines,
    `if grep -Eq '^ARG[[:space:]]+CODE_QUEUE_BASE_IMAGE([=[:space:]]|$)' "${dockerfileVariable}" 2>/dev/null; then if [ "$base_image_found" = "1" ]; then base_args=(--build-arg "CODE_QUEUE_BASE_IMAGE=$build_base_image"); echo target_build_base_image=$build_base_image; else echo target_build_base_image=default; fi; else echo target_build_base_image=unsupported; fi`,
  ];
}

export function dockerBuildTimeoutMs(service: UniDeskMicroserviceConfig, options: DeployOptions): number {
  const maxBuildMs = service.id === "code-queue" ? 1_800_000 : 540_000;
  return Math.min(options.timeoutMs, maxBuildMs);
}

export function devK3sPrepullImages(service: UniDeskMicroserviceConfig): string[] {
  if (!isDevK3sDeployService(service)) return [];
  if (service.id === "backend-core") return ["rust:1-bookworm", "postgres:16-bookworm"];
  return ["oven/bun:1-alpine"];
}
