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

// Moved mechanically from scripts/src/ci.ts:592-750 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 { DispatchResult, PublishPreflight, PublishPreflightChannelProbe, PublishPreflightControlChannel, PublishPreflightControlChannelProbe, PublishPreflightDetailedChannel, PublishPreflightFailureClassification, PublishPreflightTransport } from "./types";
import { status } from "./cleanup";
import { d601ProviderId } from "./types";

export function chunks(value: string, size: number): string[] {
  const result: string[] = [];
  for (let index = 0; index < value.length; index += size) {
    result.push(value.slice(index, index + size));
  }
  return result;
}

export const hostSshBase64UploadChunkChars = 3000;

export function asRecord(value: unknown): Record<string, unknown> | null {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}

export function asString(value: unknown): string {
  return typeof value === "string" ? value : "";
}

export function coreBody(response: unknown): Record<string, unknown> | null {
  return asRecord(asRecord(response)?.body);
}

export function emitCiInstallProgress(stage: string, status: "started" | "skipped" | "succeeded" | "failed", detail: Record<string, unknown> = {}): void {
  console.error(JSON.stringify({
    event: "ci.install.progress",
    at: new Date().toISOString(),
    stage,
    status,
    ...detail,
  }));
}

export function responseOk(response: unknown): boolean {
  if (typeof response !== "object" || response === null) return false;
  const record = response as Record<string, unknown>;
  if ("ok" in record && record.ok === false) return false;
  const body = asRecord(record.body);
  if (body !== null && "ok" in body && body.ok === false) return false;
  return true;
}

export function channelProbe(
  channel: PublishPreflightDetailedChannel,
  controlChannel: PublishPreflightControlChannel,
  ok: boolean,
  requiredFor: string,
  detail: unknown,
): PublishPreflightChannelProbe {
  return { channel, controlChannel, ok, requiredFor, detail };
}

export const publishPreflightControlChannelOrder: PublishPreflightControlChannel[] = ["backend-core", "database", "provider", "registry"];

export function summarizePublishControlChannels(channels: PublishPreflightChannelProbe[]): PublishPreflightControlChannelProbe[] {
  return publishPreflightControlChannelOrder.map((channel) => {
    const probes = channels.filter((item) => item.controlChannel === channel);
    return {
      channel,
      ok: probes.length > 0 && probes.every((item) => item.ok),
      requiredFor: Array.from(new Set(probes.map((item) => item.requiredFor))),
      probes: probes.map((item) => item.channel),
    };
  });
}

export function backendCoreUnavailable(value: unknown): boolean {
  const record = asRecord(value);
  const body = coreBody(value);
  if (record?.runnerDisposition === "infra-blocked") return true;
  if (record?.failureKind === "target-stack-not-running") return true;
  if (body?.runnerDisposition === "infra-blocked") return true;
  if (body?.failureKind === "target-stack-not-running") return true;
  if (body?.degradedReason === "backend-core-container-missing") return true;
  const text = JSON.stringify(value) ?? "";
  return text.includes("No such container: unidesk-backend-core")
    || text.includes("No such container: unidesk-database");
}

export function transportKind(transport: PublishPreflightTransport): "local-docker" | "remote-frontend" | "provider-tunnel" {
  return transport.kind ?? "local-docker";
}

export function classifyPublishPreflightFailure(
  transport: PublishPreflightTransport,
  overview: unknown,
  sshProbe: DispatchResult,
  registry: unknown,
): PublishPreflightFailureClassification | null {
  const kind = transportKind(transport);
  const overviewRecord = asRecord(overview);
  if (overviewRecord?.failureClassification === "auth-missing") return "auth-missing";
  if (overviewRecord?.failureClassification === "remote-proxy-missing") return "remote-proxy-missing";
  if (overviewRecord?.failureClassification === "provider-unreachable") return "provider-unreachable";
  if (kind === "local-docker" && backendCoreUnavailable(overview)) return "local-docker-required";
  if (kind !== "local-docker" && !responseOk(overview)) return "remote-proxy-missing";
  if (!sshProbe.ok) return "provider-unreachable";
  const registryRecord = asRecord(registry);
  if (registryRecord?.failureClassification === "local-docker-required") return "local-docker-required";
  if (registryRecord?.failureClassification === "provider-ssh-command-missing") return "provider-unreachable";
  if (registryRecord?.failureClassification === "ssh-helper-command-shape-incompatible") return "ssh-helper-command-shape-incompatible";
  if (registryRecord?.failureClassification === "remote-command-timeout") return "remote-command-timeout";
  if (registryRecord?.failureClassification === "registry-not-installed") return "registry-not-installed";
  if (registryRecord?.failureClassification === "registry-unhealthy") return "registry-unhealthy";
  if (registryRecord?.ok === false) return kind === "local-docker" ? "provider-unreachable" : "registry-unhealthy";
  return null;
}

export function recommendedPublishPreflightAction(
  failureClassification: PublishPreflightFailureClassification | null,
  registry: unknown,
  missingControlChannels: PublishPreflightControlChannel[],
): string {
  if (failureClassification === null && missingControlChannels.length === 0) return "none";
  const registryRecommendedAction = asString(asRecord(registry)?.recommendedAction);
  if (registryRecommendedAction.length > 0 && registryRecommendedAction !== "none") return registryRecommendedAction;
  if (failureClassification === "local-docker-required") return "rerun this read-only preflight through --main-server-ip <host> or from a main-server CLI with backend-core available";
  if (failureClassification === "auth-missing") return "restore frontend control-plane authentication before rerunning the dry-run preflight";
  if (failureClassification === "remote-proxy-missing") return "restore frontend to backend-core proxy reachability before rerunning the dry-run preflight";
  if (failureClassification === "provider-unreachable") return "restore D601 provider-gateway host.ssh reachability before rerunning the dry-run preflight";
  if (failureClassification === "ssh-helper-command-shape-incompatible") return "upgrade or shorten the host.ssh readonly command shape before rerunning the dry-run preflight";
  if (failureClassification === "remote-command-timeout") return "rerun artifact-registry health and inspect D601 host.ssh latency if the timeout repeats";
  if (failureClassification === "registry-not-installed") return "install the D601 artifact registry through the controlled artifact-registry install path, then rerun health";
  if (failureClassification === "registry-unhealthy") return "restore the D601 artifact registry service/container/API until artifact-registry health passes";
  if (failureClassification === "ci-runner-not-ready") return "restore D601 unidesk-ci Tekton runner prerequisites before authorizing backend-core artifact publication";
  if (missingControlChannels.includes("registry")) return "restore or install the D601 artifact registry until artifact-registry health passes";
  return `restore missing control channel(s): ${missingControlChannels.join(", ") || "unknown"}`;
}

export function publishPreflightControlPlane(
  transport: PublishPreflightTransport,
  failureClassification: PublishPreflightFailureClassification | null,
  remoteCommandShape = "bun scripts/cli.ts --main-server-ip <host> ci publish-user-service --service <id> --commit <full-sha> --dry-run",
): Record<string, unknown> {
  const kind = transportKind(transport);
  const remoteCapable = kind !== "local-docker";
  return {
    transport: kind,
    remoteCapable,
    remoteHost: transport.remoteHost ?? null,
    localDockerRequired: kind === "local-docker",
    failureClassification,
    preferredRunnerPath: "frontend-private-proxy",
    fallbackRunnerPath: "main-server-local-docker",
    remoteCommandShape,
    providerTunnel: {
      providerId: d601ProviderId,
      command: "host.ssh",
      requiredFor: ["provider-host-ssh", "artifact-registry"],
    },
  };
}

export function publishPreflightFailedScopes(preflight: PublishPreflight): string[] {
  if (preflight.ok) return [];
  const registryScopes = asRecord(preflight.registry)?.failedScopes;
  if (Array.isArray(registryScopes)) return registryScopes.map(String);
  return preflight.missingChannels;
}
