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

// Moved mechanically from scripts/src/ci.ts:751-887 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, CiPublishBackendCoreOptions, DispatchResult } from "./types";
import { shellQuote } from "./options";
import { backendCoreUnavailable } from "./preflight";
import { dispatchSsh } from "./remote";
import { d601Kubeconfig } from "./types";

export function ciRunnerPreflightScript(sourceHostPath: string): string {
  return [
    "set -eu",
    ...d601K3sGuardShellLines(d601Kubeconfig),
    "printf 'provider_host_ssh=ok\\n'",
    "printf 'kubectl='",
    "command -v kubectl >/dev/null && printf 'ok\\n' || { printf 'missing\\n'; exit 127; }",
    "printf 'docker='",
    "command -v docker >/dev/null && printf 'ok\\n' || { printf 'missing\\n'; exit 127; }",
    "printf 'docker_socket='",
    "if [ -S /var/run/docker.sock ] || [ -S /run/docker.sock ]; then printf 'true\\n'; else printf 'false\\n'; fi",
    "printf 'namespace='",
    "kubectl get namespace unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
    "printf 'tekton_pipeline='",
    "kubectl get pipeline/unidesk-backend-core-artifact-publish -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
    "printf 'tekton_task='",
    "kubectl get task/unidesk-backend-core-artifact-publish -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
    "printf 'service_account='",
    "kubectl get serviceaccount/unidesk-ci-runner -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
    "printf 'pvc='",
    "kubectl get pvc/unidesk-ci-cache -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
    "printf 'source_parent_directory='",
    `test -d ${shellQuote(posixPath.dirname(sourceHostPath))} && printf 'true\\n' || printf 'false\\n'`,
  ].join("\n");
}

export function keyValueBool(stdout: string, key: string): boolean {
  const match = new RegExp(`^${key}=(.*)$`, "mu").exec(stdout);
  if (match === null) return false;
  const value = match[1]?.trim().toLowerCase() ?? "";
  return value === "true" || value === "ok" || value === "pass" || value.startsWith("pass ");
}

export function backendCoreCiRunnerReady(result: DispatchResult): boolean {
  return result.ok
    && keyValueBool(result.stdout, "d601_native_k3s_guard")
    && keyValueBool(result.stdout, "kubectl")
    && keyValueBool(result.stdout, "docker")
    && keyValueBool(result.stdout, "namespace")
    && keyValueBool(result.stdout, "tekton_pipeline")
    && keyValueBool(result.stdout, "tekton_task")
    && keyValueBool(result.stdout, "service_account")
    && keyValueBool(result.stdout, "pvc")
    && keyValueBool(result.stdout, "source_parent_directory");
}

export function backendCoreArtifactRequirements(options: CiPublishBackendCoreOptions, plannedArtifact: ArtifactSummary): Record<string, unknown> {
  return {
    requiredLabels: {
      "unidesk.ai/service-id": "backend-core",
      "unidesk.ai/source-commit": options.commit,
      "unidesk.ai/source-repo": options.repoUrl,
      "unidesk.ai/dockerfile": options.dockerfile,
    },
    digest: {
      requiredAfterPublish: true,
      dryRunValue: plannedArtifact.digest,
      fields: ["artifactSummary.digest", "artifactSummary.digestRef"],
      source: "D601 registry manifest HEAD after ci publish-backend-core succeeds",
    },
    imageRef: plannedArtifact.imageRef,
  };
}

export function backendCoreDevApplyPath(options: CiPublishBackendCoreOptions, plannedArtifact: ArtifactSummary): Record<string, unknown> {
  return {
    environment: "dev",
    pullOnly: true,
    apply: `bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ${options.commit}`,
    dryRun: `bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ${options.commit} --dry-run`,
    sourceImage: plannedArtifact.imageRef,
    forbidden: ["--env prod", "cargo build", "docker build", "docker compose build", "server rebuild backend-core"],
    note: "PROD is intentionally not part of backend-core publish preflight; dev apply still needs explicit authorization after artifact publication.",
  };
}

export function backendCoreRemoteCommandShape(commit: string): string {
  return `bun scripts/cli.ts --main-server-ip <host> ci publish-backend-core --commit ${commit} --dry-run`;
}

export function dispatchPreflightFailure(command: string, result: DispatchResult): DispatchResult {
  return {
    ok: false,
    taskId: result.taskId,
    status: result.status,
    stdout: result.stdout.slice(-4000),
    stderr: result.stderr.slice(-4000),
    exitCode: result.exitCode,
    raw: {
      command,
      taskId: result.taskId,
      status: result.status,
      exitCode: result.exitCode,
      stderrTail: result.stderr.slice(-1200),
      stdoutTail: result.stdout.slice(-1200),
      raw: result.raw,
    },
  };
}

export function commandResultFromDispatch(command: string[], cwd: string, result: DispatchResult) {
  return {
    command,
    cwd,
    exitCode: result.exitCode,
    stdout: result.stdout,
    stderr: result.stderr,
    signal: null,
    timedOut: result.status === "timeout",
  };
}

export async function dispatchReadonlySsh(command: string, waitMs: number, remoteTimeoutMs: number): Promise<DispatchResult> {
  try {
    const result = await dispatchSsh(command, waitMs, remoteTimeoutMs);
    if (!result.ok && backendCoreUnavailable(result.raw)) {
      return {
        ...result,
        status: "infra-blocked",
        stderr: "backend-core bridge unavailable while dispatching readonly SSH task",
      };
    }
    return result;
  } catch (error) {
    return {
      ok: false,
      taskId: null,
      status: null,
      stdout: "",
      stderr: error instanceof Error ? error.message : String(error),
      exitCode: null,
      raw: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack ?? null } : String(error),
    };
  }
}

export function artifactRegistryProbeCommand(probe: ArtifactRegistryReadonlyProbe): string[] {
  return [process.execPath, "scripts/cli.ts", "ssh", probe.providerId, "argv", "bash", "-lc", probe.script];
}
