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

// Moved mechanically from scripts/src/ci.ts:2568-2959 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 { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-guard";
import { runSshCommandCapture } from "../ssh";

import type { ArtifactSummaryContext, CiOptions, CiPublishBackendCoreOptions, CiPublishUserServiceArtifactOptions, PublishPreflightTransport } from "./types";
import { artifactSummaryDefaults } from "./artifact-summary";
import { publishUserServiceArtifactDirectDocker } from "./direct-docker";
import { assertUserServicePublishProducer, blockedArtifactResult, blockedReason, numberOption, publishTransportOption, repoConnectivityProbeUrl, repoNeedsGithubSshIdentity, repoSshUrl, requireFullCommit, requireRepoRelativePath, requireServiceId, resolveCatalogArtifact, stringOption, userServicePublishBoundaryBlock } from "./options";
import { backendCoreArtifactPipelineRunManifest, backendCoreArtifactSourceHostPath, pipelineRunManifest, userServiceArtifactPipelineRunManifest, userServiceArtifactSourceHostPath } from "./pipelinerun";
import { pipelineRunWaitSucceeded, readPipelineRunCondition, remoteCreatePipelineRun, waitForPipelineRun } from "./pipelinerun-runtime";
import { publishPreflightControlChannelOrder, publishPreflightFailedScopes } from "./preflight";
import { assertArtifactSummaryComplete, publishBackendCorePreflight, publishUserServicePreflight, readArtifactSummaryFromPipelineRun } from "./publish-preflight";
import { localPublishPreflightTransport } from "./remote";
import { backendCoreArtifactRequirements, backendCoreDevApplyPath } from "./runner-preflight";
import { prepareBackendCoreArtifactSource, prepareClaudeqqArtifactSource, prepareUserServiceArtifactSource } from "./source-prepare";
import { defaultCiProviderId, providerGatewayWsEgressProxyUrl } from "./types";

export async function run(options: CiOptions): Promise<Record<string, unknown>> {
  const name = await remoteCreatePipelineRun(pipelineRunManifest(options), options.target);
  const wait = await waitForPipelineRun(name, options.waitMs, options.target);
  const condition = wait === null ? null : await readPipelineRunCondition(name, options.target);
  const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
  return {
    ok: waitSucceeded,
    providerId: options.target.providerId,
    pipelineRun: name,
    namespace: "unidesk-ci",
    repoUrl: options.repoUrl,
    revision: options.revision,
    wait: wait === null ? null : {
      ok: waitSucceeded,
      dispatchOk: wait.ok,
      dispatchStatus: wait.status,
      dispatchExitCode: wait.exitCode,
      stdoutTail: wait.stdout.slice(-6000),
      stderrTail: wait.stderr.slice(-6000),
    },
    condition,
    next: [
      `bun scripts/cli.ts ci logs ${name} --target ${options.target.targetId}`,
      `bun scripts/cli.ts ci status --target ${options.target.targetId}`,
    ],
  };
}

export async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
  const summaryContext: ArtifactSummaryContext = {
    serviceId: "backend-core",
    commit: options.commit,
    repoUrl: options.repoUrl,
    dockerfile: options.dockerfile,
    imageRepository: options.imageRepository,
  };
  const plannedArtifact = artifactSummaryDefaults(summaryContext);
  if (options.dryRun) {
    return runCiPublishBackendCoreDryRunPreflight(config, [
      "publish-backend-core",
      "--commit",
      options.commit,
      "--dry-run",
      ...(options.waitMs > 0 ? ["--wait-ms", String(options.waitMs)] : []),
    ], localPublishPreflightTransport, options);
  }
  const source = await prepareBackendCoreArtifactSource(config, options);
  const name = await remoteCreatePipelineRun(backendCoreArtifactPipelineRunManifest(options));
  const wait = await waitForPipelineRun(name, options.waitMs);
  const condition = wait === null ? null : await readPipelineRunCondition(name);
  const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
  const artifact = waitSucceeded && wait !== null
    ? await readArtifactSummaryFromPipelineRun(name, summaryContext)
    : plannedArtifact;
  if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
  return {
    ok: waitSucceeded,
    pipelineRun: name,
    namespace: "unidesk-ci",
    repoUrl: options.repoUrl,
    commit: options.commit,
    source,
    artifact: artifact.imageRef,
    artifactSummary: artifact,
    boundary: "CI publishes the image to D601 registry; CD must pull it and must not build backend-core",
    wait: wait === null ? null : {
      ok: waitSucceeded,
      dispatchOk: wait.ok,
      dispatchStatus: wait.status,
      dispatchExitCode: wait.exitCode,
      stdoutTail: wait.stdout.slice(-6000),
      stderrTail: wait.stderr.slice(-6000),
    },
    condition,
    next: [
      `bun scripts/cli.ts ci logs ${name}`,
      `bun scripts/cli.ts deploy apply --env prod --service backend-core --commit ${options.commit}`,
    ],
  };
}

export async function publishUserServiceArtifact(config: UniDeskConfig, options: CiPublishUserServiceArtifactOptions): Promise<Record<string, unknown>> {
  const summaryContext: ArtifactSummaryContext = {
    serviceId: options.serviceId,
    dockerfile: options.dockerfile,
    commit: options.commit,
    repoUrl: options.repoUrl,
    imageRepository: options.imageRepository,
  };
  const plannedArtifact = artifactSummaryDefaults(summaryContext);
  const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
  if (options.dryRun) {
    const preflight = await publishUserServicePreflight(config, options, plannedArtifact, localPublishPreflightTransport);
    return {
      ok: preflight.ok,
      mode: "dry-run-preflight",
      runnerDisposition: preflight.runnerDisposition,
      pipeline: "unidesk-user-service-artifact-publish",
      namespace: "unidesk-ci",
      repoUrl: options.repoUrl,
      commit: options.commit,
      serviceId: options.serviceId,
      supportedArtifactPublish: preflight.supportedArtifactPublish,
      missingChannels: preflight.missingChannels,
      missingControlChannels: preflight.missingControlChannels,
      controlChannels: preflight.controlChannels,
      channels: preflight.channels,
      failureClassification: preflight.failureClassification,
      failedScopes: publishPreflightFailedScopes(preflight),
      recommendedAction: preflight.recommendedAction,
      remoteCommandShape: preflight.remoteCommandShape,
      controlPlane: preflight.controlPlane,
      registry: preflight.registry,
      sourceHostPath: options.sourceHostPath,
      source: {
        ok: preflight.ok,
        mode: "planned-only",
        providerId: defaultCiProviderId,
        repoUrl: options.repoUrl,
        repoFetchUrl: plannedRepoFetchUrl,
        repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
        commit: options.commit,
        serviceId: options.serviceId,
        dockerfile: options.dockerfile,
        imageRepository: options.imageRepository,
        sourceHostPath: options.sourceHostPath,
        ...(options.serviceId === "claudeqq" ? { overlay: "UniDesk claudeqq Dockerfile and unidesk-adapter.cjs are injected before Tekton build" } : {}),
      },
      artifact: plannedArtifact.imageRef,
      artifactSummary: plannedArtifact,
      controlledPublish: {
        environment: "D601",
        namespace: "unidesk-ci",
        pipeline: "unidesk-user-service-artifact-publish",
        command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --wait-ms 1200000 --transport ${options.transport}`,
        requiresReadyControlChannels: publishPreflightControlChannelOrder,
      },
      boundary: preflight.boundary,
      next: preflight.next,
    };
  }
  if (options.transport === "direct-docker" || (options.transport === "auto" && options.serviceId === "code-queue" && options.repoUrl === "https://github.com/pikasTech/unidesk")) {
    return publishUserServiceArtifactDirectDocker(options, summaryContext);
  }
  const source = options.serviceId === "claudeqq"
    ? await prepareClaudeqqArtifactSource(config, options)
    : await prepareUserServiceArtifactSource(config, options);
  const name = await remoteCreatePipelineRun(userServiceArtifactPipelineRunManifest(options));
  const wait = await waitForPipelineRun(name, options.waitMs);
  const condition = wait === null ? null : await readPipelineRunCondition(name);
  const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
  const artifact = waitSucceeded && wait !== null
    ? await readArtifactSummaryFromPipelineRun(name, summaryContext)
    : plannedArtifact;
  if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
  return {
    ok: waitSucceeded,
    pipelineRun: name,
    namespace: "unidesk-ci",
    repoUrl: options.repoUrl,
    commit: options.commit,
    serviceId: options.serviceId,
    source,
    artifact: artifact.imageRef,
    artifactSummary: artifact,
    boundary: "CI publishes the user-service image to the D601 registry only; it must not deploy production or mutate the production namespace",
    wait: wait === null ? null : {
      ok: waitSucceeded,
      dispatchOk: wait.ok,
      dispatchStatus: wait.status,
      dispatchExitCode: wait.exitCode,
      stdoutTail: wait.stdout.slice(-6000),
      stderrTail: wait.stderr.slice(-6000),
    },
    condition,
    next: [
      `bun scripts/cli.ts ci logs ${name}`,
      "use artifactSummary.imageRef or artifactSummary.digestRef as later dev/prod deployment input",
    ],
  };
}

export async function runCiPublishUserServiceDryRunPreflight(
  config: UniDeskConfig,
  args: string[],
  transport: PublishPreflightTransport,
): Promise<Record<string, unknown>> {
  const serviceId = requireServiceId(stringOption(args, "--service") ?? stringOption(args, "--service-id"));
  const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
  if (!args.includes("--dry-run")) throw new Error("publish-user-service preflight requires --dry-run");
  if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
    throw new Error("ci publish-user-service reads source repo from CI.json; edit CI.json instead of using --repo");
  }
  const artifact = resolveCatalogArtifact(serviceId);
  if (artifact.kind === "source-build" && artifact.serviceId === "backend-core") {
    throw new Error("backend-core uses ci publish-backend-core; publish-user-service is for registered user services");
  }
  if (artifact.kind === "upstream-image") {
    return blockedArtifactResult(artifact, commit, artifact.blockedReason);
  }
  if (artifact.status === "blocked") {
    return blockedArtifactResult(artifact, commit, blockedReason(artifact));
  }
  assertUserServicePublishProducer(artifact);
  const dockerfile = requireRepoRelativePath(artifact.source.dockerfile, `CI.json.artifacts.${serviceId}.source.dockerfile`);
  const boundaryBlock = userServicePublishBoundaryBlock(config, serviceId, commit, artifact);
  if (boundaryBlock !== null) return boundaryBlock;
  const summaryContext: ArtifactSummaryContext = {
    serviceId,
    dockerfile,
    commit,
    repoUrl: artifact.source.repo,
    imageRepository: artifact.image.repository,
  };
  const plannedArtifact = artifactSummaryDefaults(summaryContext);
  const options: CiPublishUserServiceArtifactOptions = {
    repoUrl: artifact.source.repo,
    commit,
    waitMs: numberOption(args, "--wait-ms", 0),
    serviceId,
    dockerfile,
    imageRepository: artifact.image.repository,
    sourceHostPath: userServiceArtifactSourceHostPath(serviceId, commit),
    dryRun: true,
    transport: publishTransportOption(stringOption(args, "--transport")),
  };
  const preflight = await publishUserServicePreflight(config, options, plannedArtifact, transport);
  const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
  return {
    ok: preflight.ok,
    mode: "dry-run-preflight",
    runnerDisposition: preflight.runnerDisposition,
    pipeline: "unidesk-user-service-artifact-publish",
    namespace: "unidesk-ci",
    repoUrl: options.repoUrl,
    commit: options.commit,
    serviceId: options.serviceId,
    supportedArtifactPublish: preflight.supportedArtifactPublish,
    missingChannels: preflight.missingChannels,
    missingControlChannels: preflight.missingControlChannels,
    controlChannels: preflight.controlChannels,
    channels: preflight.channels,
    failureClassification: preflight.failureClassification,
    failedScopes: publishPreflightFailedScopes(preflight),
    recommendedAction: preflight.recommendedAction,
    remoteCommandShape: preflight.remoteCommandShape,
    controlPlane: preflight.controlPlane,
    registry: preflight.registry,
    sourceHostPath: options.sourceHostPath,
    source: {
      ok: preflight.ok,
      mode: "planned-only",
      providerId: defaultCiProviderId,
      repoUrl: options.repoUrl,
      repoFetchUrl: plannedRepoFetchUrl,
      repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
      commit: options.commit,
      serviceId: options.serviceId,
      dockerfile: options.dockerfile,
      imageRepository: options.imageRepository,
      sourceHostPath: options.sourceHostPath,
      ...(options.serviceId === "claudeqq" ? { overlay: "UniDesk claudeqq Dockerfile and unidesk-adapter.cjs are injected before Tekton build" } : {}),
    },
    artifact: plannedArtifact.imageRef,
    artifactSummary: plannedArtifact,
    controlledPublish: {
      environment: "D601",
      namespace: "unidesk-ci",
      pipeline: "unidesk-user-service-artifact-publish",
      command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --wait-ms 1200000 --transport ${options.transport}`,
      requiresReadyControlChannels: publishPreflightControlChannelOrder,
    },
    boundary: preflight.boundary,
    next: preflight.next,
  };
}

export async function runCiPublishBackendCoreDryRunPreflight(
  config: UniDeskConfig,
  args: string[],
  transport: PublishPreflightTransport,
  resolvedOptions?: CiPublishBackendCoreOptions,
): Promise<Record<string, unknown>> {
  const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
  if (!args.includes("--dry-run")) throw new Error("publish-backend-core preflight requires --dry-run");
  if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
    throw new Error("ci publish-backend-core reads source repo from CI.json; edit CI.json instead of using --repo");
  }
  const artifact = resolveCatalogArtifact("backend-core");
  if (artifact.kind !== "source-build") throw new Error("backend-core must be modeled as a source-build artifact in CI.json");
  if (artifact.status === "blocked") return blockedArtifactResult(artifact, commit, blockedReason(artifact));
  const dockerfile = requireRepoRelativePath(artifact.source.dockerfile, "CI.json.artifacts.backend-core.source.dockerfile");
  const options: CiPublishBackendCoreOptions = resolvedOptions ?? {
    repoUrl: artifact.source.repo,
    commit,
    waitMs: numberOption(args, "--wait-ms", 0),
    sourceHostPath: backendCoreArtifactSourceHostPath(commit),
    dockerfile,
    imageRepository: artifact.image.repository,
    dryRun: true,
  };
  const summaryContext: ArtifactSummaryContext = {
    serviceId: "backend-core",
    commit,
    repoUrl: options.repoUrl,
    dockerfile: options.dockerfile,
    imageRepository: options.imageRepository,
  };
  const plannedArtifact = artifactSummaryDefaults(summaryContext);
  const preflight = await publishBackendCorePreflight(config, options, plannedArtifact, transport);
  const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
  const blockedScopes = publishPreflightFailedScopes(preflight);
  return {
    ok: preflight.ok,
    mode: "dry-run-preflight",
    runnerDisposition: preflight.runnerDisposition,
    pipeline: "unidesk-backend-core-artifact-publish",
    namespace: "unidesk-ci",
    repoUrl: options.repoUrl,
    commit: options.commit,
    targetCommit: options.commit,
    sourceRepo: options.repoUrl,
    serviceId: "backend-core",
    providerId: defaultCiProviderId,
    ciRunner: {
      providerId: defaultCiProviderId,
      environment: "D601",
      namespace: "unidesk-ci",
      pipeline: "unidesk-backend-core-artifact-publish",
      wouldBuildOnD601: true,
      realBuildRequiresAuthorization: true,
    },
    registryTarget: plannedArtifact.repository,
    wouldBuildOnD601: true,
    dryRunBuildStarted: false,
    supportedArtifactPublish: preflight.supportedArtifactPublish,
    missingChannels: preflight.missingChannels,
    missingControlChannels: preflight.missingControlChannels,
    controlChannels: preflight.controlChannels,
    channels: preflight.channels,
    failureClassification: preflight.failureClassification,
    failedScopes: blockedScopes,
    blockedScopes,
    recommendedAction: preflight.recommendedAction,
    remoteCommandShape: preflight.remoteCommandShape,
    remoteControlPlaneCandidate: preflight.controlPlane,
    controlPlane: preflight.controlPlane,
    registry: preflight.registry,
    sourceHostPath: options.sourceHostPath,
    source: {
      ok: preflight.ok,
      mode: "planned-only",
      providerId: defaultCiProviderId,
      repoUrl: options.repoUrl,
      repoFetchUrl: plannedRepoFetchUrl,
      repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
      commit: options.commit,
      serviceId: "backend-core",
      dockerfile: options.dockerfile,
      imageRepository: options.imageRepository,
      sourceHostPath: options.sourceHostPath,
    },
    sourceAuth: {
      repoFetchUrl: plannedRepoFetchUrl,
      egressProxy: providerGatewayWsEgressProxyUrl,
      gitSshProxy: repoNeedsGithubSshIdentity(plannedRepoFetchUrl),
      identityRequired: repoNeedsGithubSshIdentity(plannedRepoFetchUrl),
    },
    d601Ci: {
      providerId: defaultCiProviderId,
      namespace: "unidesk-ci",
      pipeline: "unidesk-backend-core-artifact-publish",
      wouldBuildOnD601: true,
      dryRunWillStartTekton: false,
      dryRunWillCompileRust: false,
      dryRunWillPushRegistry: false,
    },
    artifact: plannedArtifact.imageRef,
    artifactSummary: plannedArtifact,
    artifactRequirements: backendCoreArtifactRequirements(options, plannedArtifact),
    controlledPublish: {
      environment: "D601",
      namespace: "unidesk-ci",
      pipeline: "unidesk-backend-core-artifact-publish",
      command: `bun scripts/cli.ts ci publish-backend-core --commit ${options.commit} --wait-ms 1200000`,
      requiresReadyControlChannels: publishPreflightControlChannelOrder,
      requiresHumanAuthorization: true,
    },
    devApplyPath: backendCoreDevApplyPath(options, plannedArtifact),
    boundary: preflight.boundary,
    next: preflight.next,
  };
}
