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

// Moved mechanically from scripts/src/ci.ts:2125-2316 for #903.

import { randomUUID } from "node:crypto";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, posix as posixPath } from "node:path";
import { blockedCatalogArtifactIds, catalogSummary, findCiCatalogArtifact, loadCiCatalog, supportedSourceBuildArtifactIds, type CiCatalogArtifact, type CiSourceBuildCatalogArtifact, type CiUpstreamImageCatalogArtifact } from "../ci-catalog";
import { runCommand } from "../command";
import { type UniDeskConfig, repoRoot, rootPath } from "../config";
import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "../deploy-ssh-identity";
import { jobWithTail, listJobs, readJob, startJob } from "../jobs";
import { coreInternalFetch } from "../microservices";
import {
  artifactRegistryReadonlyResultFromCommand,
  buildArtifactRegistryReadonlyProbe,
  parseArtifactRegistryOptions,
  type ArtifactRegistryReadonlyProbe,
} from "../artifact-registry";
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard";
import { runSshCommandCapture } from "../ssh";

import type { ArtifactSummary, ArtifactSummaryContext, CiPublishUserServiceArtifactOptions } from "./types";
import { artifactSummaryDefaults, assertCommandOk, directDockerRegistryCurl, dockerArtifactDigest, registryManifestDigest } from "./artifact-summary";
import { assertArtifactSummaryComplete } from "./publish-preflight";
import { codeQueueDirectDockerBaseImage, providerGatewayWsEgressProxyUrl } from "./types";

export function buildContextForService(serviceId: string, dockerfile: string): string {
  return serviceId === "claudeqq" ? posixPath.dirname(dockerfile) : ".";
}

export function directDockerBuildNetworkArgs(serviceId: string): string[] {
  if (serviceId !== "code-queue") return [];
  const noProxy = [
    "localhost",
    "127.0.0.1",
    "::1",
    "host.docker.internal",
    "d601-tcp-egress-gateway",
    "d601-tcp-egress-gateway.unidesk",
    "d601-tcp-egress-gateway.unidesk.svc",
    "d601-tcp-egress-gateway.unidesk.svc.cluster.local",
    "backend-core",
    "oa-event-flow",
    "database",
  ].join(",");
  return [
    "--network",
    "host",
    "--build-arg",
    `HTTP_PROXY=${providerGatewayWsEgressProxyUrl}`,
    "--build-arg",
    `HTTPS_PROXY=${providerGatewayWsEgressProxyUrl}`,
    "--build-arg",
    `http_proxy=${providerGatewayWsEgressProxyUrl}`,
    "--build-arg",
    `https_proxy=${providerGatewayWsEgressProxyUrl}`,
    "--build-arg",
    `NO_PROXY=${noProxy}`,
    "--build-arg",
    `no_proxy=${noProxy}`,
  ];
}

export function directDockerSourceGitMode(repoUrl: string): "local-unidesk-worktree" | "git-archive" {
  return repoUrl === "https://github.com/pikasTech/unidesk" ? "local-unidesk-worktree" : "git-archive";
}

export async function prepareDirectDockerUserServiceSource(options: CiPublishUserServiceArtifactOptions): Promise<{ path: string; cleanup: () => void; summary: Record<string, unknown> }> {
  const tempRoot = mkdtempSync(join(tmpdir(), `unidesk-ci-${options.serviceId}-${options.commit.slice(0, 8)}-`));
  const sourcePath = join(tempRoot, "source");
  const gitMode = directDockerSourceGitMode(options.repoUrl);
  if (gitMode !== "local-unidesk-worktree") {
    rmSync(tempRoot, { recursive: true, force: true });
    throw new Error("direct-docker publish currently supports only UniDesk repo-owned source-build artifacts; use --transport tekton for external repositories");
  }
  const resolved = runCommand(["git", "rev-parse", "--verify", `${options.commit}^{commit}`], repoRoot, { timeoutMs: 30_000 });
  assertCommandOk(resolved, "resolve source commit");
  if (resolved.stdout.trim() !== options.commit) {
    rmSync(tempRoot, { recursive: true, force: true });
    throw new Error(`direct-docker source commit mismatch: resolved ${resolved.stdout.trim()} expected ${options.commit}`);
  }
  const dockerfileExists = runCommand(["git", "cat-file", "-e", `${options.commit}:${options.dockerfile}`], repoRoot, { timeoutMs: 30_000 });
  assertCommandOk(dockerfileExists, "verify source dockerfile");
  const worktree = runCommand(["git", "worktree", "add", "--detach", sourcePath, options.commit], repoRoot, { timeoutMs: 120_000 });
  if (worktree.exitCode !== 0) {
    rmSync(tempRoot, { recursive: true, force: true });
    throw new Error(`prepare source worktree failed: ${worktree.stderr || worktree.stdout}`);
  }
  return {
    path: sourcePath,
    cleanup: () => {
      const removed = runCommand(["git", "worktree", "remove", "--force", sourcePath], repoRoot, { timeoutMs: 60_000 });
      if (removed.exitCode !== 0) rmSync(tempRoot, { recursive: true, force: true });
      else rmSync(tempRoot, { recursive: true, force: true });
    },
    summary: {
      ok: true,
      mode: gitMode,
      providerId: "local",
      repoUrl: options.repoUrl,
      commit: options.commit,
      serviceId: options.serviceId,
      dockerfile: options.dockerfile,
      sourceHostPath: sourcePath,
      valuesPrinted: false,
    },
  };
}

export async function publishUserServiceArtifactDirectDocker(options: CiPublishUserServiceArtifactOptions, context: ArtifactSummaryContext): Promise<Record<string, unknown>> {
  const source = await prepareDirectDockerUserServiceSource(options);
  const planned = artifactSummaryDefaults(context);
  const localImage = `${options.imageRepository}:${options.commit}`;
  const buildContext = buildContextForService(options.serviceId, options.dockerfile);
  const networkArgs = directDockerBuildNetworkArgs(options.serviceId);
  const baseArgs = options.serviceId === "code-queue" && runCommand(["docker", "image", "inspect", codeQueueDirectDockerBaseImage], repoRoot, { timeoutMs: 30_000 }).exitCode === 0
    ? ["--build-arg", `CODE_QUEUE_BASE_IMAGE=${codeQueueDirectDockerBaseImage}`]
    : [];
  try {
    if (options.serviceId === "code-queue" && baseArgs.length === 0) {
      return {
        ok: false,
        runnerDisposition: "infra-blocked",
        failureClassification: "ci-runner-not-ready",
        failureKind: "code-queue-base-image-missing",
        serviceId: options.serviceId,
        commit: options.commit,
        artifactSummary: planned,
        source: source.summary,
        artifact: planned.imageRef,
        transport: "direct-docker",
        controlledPublish: {
          environment: "DEV-local-artifact",
          command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --transport direct-docker`,
          noRollout: true,
          noRuntimeMutation: true,
        },
        boundary: "direct-docker CI artifact publish builds and pushes a commit-pinned image only; no deploy apply, no rollout, no Code Queue restart, no active task mutation",
        next: [
          "Restore or warm local unidesk-code-queue:d601 base image, then rerun the same direct-docker publish command.",
        ],
      };
    }
    const build = runCommand([
      "docker",
      "build",
      "--label",
      `unidesk.ai/service-id=${options.serviceId}`,
      "--label",
      `unidesk.ai/source-repo=${options.repoUrl}`,
      "--label",
      `unidesk.ai/source-commit=${options.commit}`,
      "--label",
      `unidesk.ai/dockerfile=${options.dockerfile}`,
      ...networkArgs,
      ...baseArgs,
      "-t",
      localImage,
      "-t",
      planned.imageRef,
      "-f",
      options.dockerfile,
      buildContext,
    ], source.path, { timeoutMs: Math.max(options.waitMs, 20 * 60_000) });
    assertCommandOk(build, "direct docker build");
    const inspectLabels = runCommand(["docker", "image", "inspect", planned.imageRef, "--format", "{{ index .Config.Labels \"unidesk.ai/service-id\" }} {{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot, { timeoutMs: 30_000 });
    assertCommandOk(inspectLabels, "inspect built image labels");
    if (!inspectLabels.stdout.includes(`${options.serviceId} ${options.commit}`)) throw new Error(`direct docker image labels did not match ${options.serviceId}/${options.commit}`);
    const registryCheck = directDockerRegistryCurl(["-fsS", "http://127.0.0.1:5000/v2/"], 15_000);
    assertCommandOk(registryCheck, "D601 loopback artifact registry health");
    const push = runCommand(["docker", "push", planned.imageRef], repoRoot, { timeoutMs: Math.max(options.waitMs, 10 * 60_000) });
    assertCommandOk(push, "direct docker push");
    const pull = runCommand(["docker", "pull", planned.imageRef], repoRoot, { timeoutMs: 120_000 });
    assertCommandOk(pull, "direct docker pull verification");
    const digest = dockerArtifactDigest(planned.repository, planned.imageRef) ?? registryManifestDigest(planned.repository, planned.tag);
    const artifact: ArtifactSummary = {
      ...planned,
      digest,
      digestRef: digest === null ? null : `${planned.repository}@${digest}`,
    };
    assertArtifactSummaryComplete(artifact, "direct-docker");
    return {
      ok: true,
      transport: "direct-docker",
      pipelineRun: null,
      namespace: null,
      repoUrl: options.repoUrl,
      commit: options.commit,
      serviceId: options.serviceId,
      source: source.summary,
      artifact: artifact.imageRef,
      artifactSummary: artifact,
      controlledPublish: {
        environment: "DEV-local-artifact",
        command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --transport direct-docker`,
        noRollout: true,
        noRuntimeMutation: true,
        dependsOnLocalUnideskDatabase: false,
      },
      boundary: "direct-docker CI artifact publish builds and pushes a commit-pinned image only; no deploy apply, no rollout, no Code Queue restart, no active task mutation",
      wait: {
        ok: true,
        dispatchOk: null,
        dispatchStatus: null,
        dispatchExitCode: null,
        stdoutTail: push.stdout.slice(-6000),
        stderrTail: push.stderr.slice(-6000),
      },
      condition: null,
      next: [
        "use artifactSummary.imageRef or artifactSummary.digestRef as later dev artifact consumer input",
      ],
    };
  } finally {
    source.cleanup();
  }
}
