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

// Moved mechanically from scripts/src/deploy.ts:3032-3389 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 { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-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 { DeployEnvironmentManifestSource, DeployManifest, DeployManifestService, DeployOptions } from "./types";
import { readProdDeployManifestSnapshot } from "./manifest";
import { environmentPlanServiceConfig, redactedSecretContractForService } from "./paths";
import { readRuntimeState } from "./remote";
import { artifactConsumerPlanKind, artifactConsumerPlanKindFromDeployJson, artifactConsumerPlanTarget, artifactConsumerPlanTargetFromDeployJson, artifactConsumerPlanValidation, codeQueueCiCdBoundary, codeQueueSelfBootstrapGuard, databaseFingerprint, deployJsonExecutorMirrors, deploymentPathForEnvironmentService, deploymentPathForEnvironmentServiceId, environmentTargetSummary, isD601MaintenanceDeployBlocked, selectServices, unsupportedEnvironmentPlanReason, unsupportedPlanTarget, unsupportedReason } from "./service-plan";
import { artifactConsumerDryRunBlockedServiceIds, deployEnvironmentTargets, devApplySupportedServiceIds, devArtifactConsumerProdDesiredFallbackServiceIds, devArtifactConsumerServiceIds, devArtifactLiveApplyBlockedServiceIds, prodArtifactConsumerServiceIds, prodArtifactLiveApplyBlockedServiceIds } from "./types";

export async function checkOrPlan(config: UniDeskConfig, manifest: DeployManifest, options: DeployOptions, action: "check" | "plan"): Promise<Record<string, unknown>> {
  const services = selectServices(config, manifest, options.serviceId);
  const items = [];
  for (const item of services) {
    const state = await readRuntimeState(config, item.config, item.desired);
    items.push({
      ...state,
      action: action === "plan"
        ? state.supported
          ? state.upToDate ? "noop" : "deploy"
          : "unsupported"
        : undefined,
    });
  }
  return {
    ok: items.every((item) => item.supported && (action === "plan" || item.healthOk)),
    action,
    file: options.file,
    services: items,
  };
}

export function environmentDryRunPlan(
  config: UniDeskConfig | null,
  manifest: DeployManifest,
  source: DeployEnvironmentManifestSource,
  options: DeployOptions,
  action: "check" | "plan",
): Record<string, unknown> {
  const environment = options.environment;
  if (environment === null) throw new Error("environment dry-run requires --env");
  const target = deployEnvironmentTargets[environment];
  const selectedServices = options.serviceId === null
    ? manifest.services
    : manifest.services.filter((service) => service.id === options.serviceId);
  if (options.serviceId !== null && selectedServices.length === 0) {
    throw new Error(`deploy manifest ${source.ref}:deploy.json does not contain service: ${options.serviceId}`);
  }
  const services = selectedServices.map((service) => ({
    ...service,
    commitId: options.commitOverride !== null && service.id === options.serviceId ? options.commitOverride : service.commitId,
  }));
  const fingerprint = databaseFingerprint(target);
  const devUnsupported = devUnsupportedServices(manifest, options.serviceId);
  const prodArtifactUnsupported = prodArtifactUnsupportedServices(manifest, options.serviceId);
  return {
    ok: environment === "prod"
      ? services.every((service) => prodArtifactConsumerServiceIds.has(service.id))
      : services.every((service) => devApplySupportedServiceIds.has(service.id) || devArtifactConsumerServiceIds.has(service.id)),
    action,
    mode: "environment-ref-dry-run",
    dryRun: true,
    mutatesRuntime: false,
    environment,
    gitRef: source.ref,
    environmentPath: `environments.${environment}`,
    manifest: {
      source: source.source,
      path: source.path,
      ref: source.ref,
      remote: source.remote,
      branch: source.branch,
      commit: source.commit,
      blob: source.blob,
      environment: manifest.environment,
      environmentPath: `environments.${environment}`,
      fetchedAt: source.fetchedAt,
    },
    target: environmentTargetSummary(target),
    localCompatibility: {
      localFileMode: false,
      deployJsonFromWorktree: false,
      dirtyWorktreeUsed: false,
    },
    services: services.map((service) => {
      const serviceConfig = environmentPlanServiceConfig(config, service, environment);
      const unsupported = (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
        || (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
      const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
      const planKind = serviceConfig === null ? "unsupported" : artifactConsumerPlanKind(serviceConfig, environment);
      const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
      const runtimeSecrets = serviceConfig === null ? undefined : redactedSecretContractForService(config, serviceConfig, environment);
      const unsupportedReason = unsupported ? unsupportedEnvironmentPlanReason(service.id, environment) : null;
      const deployJsonContract = hasDeployJsonExecutorContract(service);
      const effectivePlanKind = deployJsonContract
        ? artifactConsumerPlanKindFromDeployJson(service, planKind)
        : planKind;
      const deployJsonPlanTarget = serviceConfig === null
        ? null
        : artifactConsumerPlanTargetFromDeployJson(service, serviceConfig, environment, planTarget);
      const drifts = deployJsonContract && serviceConfig !== null && !unsupported
        ? compareDeployJsonExecutorMirrors(service, environment, deployJsonExecutorMirrors(service, serviceConfig, environment, effectivePlanKind, deployJsonPlanTarget))
        : [];
      if (drifts.length > 0) return deployJsonDriftResult(service, environment, drifts);
      const effectiveTarget = unsupported
        ? unsupportedPlanTarget(service.id, environment, unsupportedReason ?? "unsupported")
        : deployJsonContract
          ? deployJsonPlanTarget
          : planTarget;
      const registryRepository = service.artifact?.repository ?? `unidesk/${service.id}`;
      const stableImage = service.consumer?.target.stableImage;
      const liveApplyBlockedReason = environment === "prod"
        ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null
        : devArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null;
      const dryRunOnly = unsupported || liveApplyBlockedReason !== null || dryRunBlockedReason !== null;
      return {
        id: service.id,
        repo: service.repo,
        commitId: service.commitId,
        environment,
        targetNamespace: target.namespace,
        providerId: target.provider.providerId,
        databaseFingerprint: fingerprint,
        deploymentPath: serviceConfig === null
          ? deploymentPathForEnvironmentServiceId(service.id, environment)
          : deploymentPathForEnvironmentService(serviceConfig, environment),
        artifactConsumer: serviceConfig === null ? {
          consumerKind: "unsupported",
          noRuntimeSourceBuild: true,
          dryRunOnly: true,
          blockedReason: "service is missing from config.json and no synthetic deploy service is available for this environment",
        } : {
          consumerKind: unsupported ? "unsupported" : effectivePlanKind,
          registryImage: unsupported ? null : `127.0.0.1:5000/${registryRepository}:${service.commitId}`,
          registry: unsupported ? null : {
            endpoint: "http://127.0.0.1:5000",
            repository: registryRepository,
            tag: service.commitId,
            imageRef: `127.0.0.1:5000/${registryRepository}:${service.commitId}`,
            digest: null,
            digestHeader: "Docker-Content-Digest",
            digestSource: "planned registry manifest HEAD; live apply records Docker-Content-Digest before mutation",
          },
          source: unsupported ? null : {
            repo: service.repo,
            commit: service.commitId,
            deployRef: `${source.ref}:deploy.json#environments.${environment}.services.${service.id}`,
          },
          build: unsupported ? null : {
            willCompile: effectivePlanKind === "d601-dev-target-side-build",
            willRunCargoBuild: false,
            willRunDockerBuild: effectivePlanKind === "d601-dev-target-side-build",
            willRunDockerComposeBuild: false,
            producerBoundary: service.id === "backend-core" ? "ci publish-backend-core" : "ci publish-user-service",
          },
          requiredLabels: unsupported ? null : {
            "unidesk.ai/service-id": service.id,
            "unidesk.ai/source-repo": service.repo,
            "unidesk.ai/source-commit": service.commitId,
            "unidesk.ai/dockerfile": serviceConfig.repository.dockerfile,
          },
          noRuntimeSourceBuild: unsupported || (service.consumer?.noRuntimeSourceBuild ?? effectivePlanKind !== "d601-dev-target-side-build"),
          dryRunOnly,
          blockedReason: unsupportedReason ?? dryRunBlockedReason ?? liveApplyBlockedReason,
          requiresSupervisorApproval: service.id === "code-queue" || liveApplyBlockedReason !== null,
          selfBootstrapGuard: service.id === "code-queue" ? codeQueueSelfBootstrapGuard(environment) : undefined,
          runtimeSecrets,
          sourceOfTruth: deployJsonContract ? deployJsonSourceOfTruth(service, environment) : undefined,
          driftCheck: deployJsonContract ? {
            ok: true,
            mirrors: ["deploy-executor-plan", ...(service.consumer?.kind === "d601-k3s-managed" ? ["k8s-manifest"] : [])],
          } : undefined,
        },
        target: effectiveTarget,
        runtime: service.runtime === undefined ? undefined : {
          sourceOfTruth: "deploy.json",
          containerPort: service.runtime.containerPort,
          healthPath: service.runtime.healthPath,
          memory: service.runtime.memory,
          health: service.runtime.health,
        },
        runtimeImage: stableImage === undefined ? undefined : deployJsonCommitImage(stableImage, service.commitId),
        validation: unsupported
          ? ["unsupported service remains blocked before source materialization, registry pull, manifest update, rollout, or runtime mutation"]
          : serviceConfig === null
            ? undefined
            : artifactConsumerPlanValidation(serviceConfig, environment),
        boundary: codeQueueCiCdBoundary(service.id, environment),
        unsupported: unsupported
          ? {
            ok: false,
            supported: false,
            reason: unsupportedReason,
          }
          : undefined,
        liveApply: unsupported
          ? {
            allowed: false,
            reason: unsupportedReason,
            dryRunOnly: true,
          }
          : dryRunBlockedReason !== null
          ? {
            allowed: false,
            reason: dryRunBlockedReason,
            dryRunOnly: true,
          }
          : liveApplyBlockedReason !== null
            ? {
              allowed: false,
              reason: liveApplyBlockedReason,
              dryRunOnly: true,
              requiresSupervisorApproval: true,
            }
            : environment === "prod"
              ? { allowed: prodArtifactConsumerServiceIds.has(service.id) }
              : undefined,
      };
    }),
    unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported,
  };
}

export function unsupportedDevApplyServices(manifest: DeployManifest, serviceId: string | null): string[] {
  if (manifest.environment !== "dev") return [];
  const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
  if (serviceId !== null && services.length === 0 && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId)) return [];
  return services.map((service) => service.id).filter((id) => !devApplySupportedServiceIds.has(id) && !devArtifactConsumerServiceIds.has(id));
}

export function blockedD601MaintenanceDeployServices(config: UniDeskConfig, manifest: DeployManifest, serviceId: string | null): string[] {
  return selectServices(config, manifest, serviceId)
    .map((item) => item.config)
    .filter(isD601MaintenanceDeployBlocked)
    .map((service) => service.id);
}

export function d601MaintenanceDeployBlockMessage(blocked: string[]): string {
  return `D601 target-side deployment is enabled only for k3sctl-adapter as a control-bridge maintenance path; artifact consumers must use registry CD. Blocked services: ${blocked.join(", ")}. Code Queue is dev-only through artifact-registry/deploy --env dev and has no production direct rollout path.`;
}

export function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
  if (manifest.environment !== "dev") return [];
  return selectedEnvironmentServices(manifest, serviceId).filter((service) => devArtifactConsumerServiceIds.has(service.id));
}

export function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
  if (manifest.environment !== "dev") return [];
  if (serviceId !== null && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId) && !manifest.services.some((service) => service.id === serviceId)) return [];
  return selectedEnvironmentServices(manifest, serviceId)
    .filter((service) => devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
}

export function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
  const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
  if (serviceId !== null && services.length === 0) throw new Error(`deploy manifest does not contain service: ${serviceId}`);
  return services;
}

export function selectedDevArtifactServicesWithProdFallback(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
  if (manifest.environment !== "dev" || serviceId === null) return selectedDevArtifactServices(manifest, serviceId);
  const selected = manifest.services.filter((service) => service.id === serviceId && devArtifactConsumerServiceIds.has(service.id));
  if (selected.length > 0 || !devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId)) return selected;
  const prodManifest = readProdDeployManifestSnapshot();
  return prodManifest.services.filter((service) => service.id === serviceId);
}

export function devUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
  if (manifest.environment !== "dev") return [];
  return selectedEnvironmentServices(manifest, serviceId).filter((service) => !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
}

export function prodArtifactUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
  return selectedEnvironmentServices(manifest, serviceId).filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
}

export function prodArtifactUnsupportedResult(services: DeployManifestService[]): Record<string, unknown> {
  return {
    ok: false,
    supported: false,
    error: "unsupported",
    services: services.map((service) => ({
      id: service.id,
      repo: service.repo,
      commitId: service.commitId,
      supported: false,
      reason: unsupportedEnvironmentPlanReason(service.id, "prod"),
      requiresSupervisorApproval: service.id === "code-queue",
      selfBootstrapGuard: service.id === "code-queue" ? codeQueueSelfBootstrapGuard("prod") : undefined,
      affectedRuntime: service.id === "code-queue" ? {
        dryRunMutation: false,
        plannedTarget: null,
        productionNamespaceAffected: false,
        productionSchedulerRunnerAffected: false,
        activeTaskControlAffected: false,
        activeTaskInterruptCancelAffected: false,
      } : undefined,
    })),
    policy: "prod deploy must not silently fall back to legacy D601 maintenance-channel source builds",
    supportedServices: Array.from(prodArtifactConsumerServiceIds),
  };
}

export function commitOverrideArtifactConsumerError(options: DeployOptions): string | null {
  if (options.commitOverride === null) return null;
  if (options.environment === "dev") {
    if (options.serviceId !== null && devArtifactConsumerServiceIds.has(options.serviceId)) return null;
    return `deploy --commit is supported for --env dev only on reviewed artifact consumers (${Array.from(devArtifactConsumerServiceIds).sort().join(", ")})`;
  }
  if (options.environment === "prod") {
    if (options.serviceId !== null && prodArtifactConsumerServiceIds.has(options.serviceId)) return null;
    return `deploy --commit is supported for --env prod only on reviewed artifact consumers (${Array.from(prodArtifactConsumerServiceIds).sort().join(", ")})`;
  }
  return "deploy --commit is supported only for --env dev|prod artifact consumer apply";
}

export function prodArtifactConsumerLocalManifestResult(services: DeployManifestService[]): Record<string, unknown> {
  return {
    ok: false,
    supported: false,
    error: "prod-artifact-consumer-local-manifest-blocked",
    services: services.map((service) => ({
      id: service.id,
      repo: service.repo,
      commitId: service.commitId,
      replacement: `bun scripts/cli.ts deploy apply --env prod --service ${service.id} --commit ${service.commitId}`,
      reason: "production artifact consumers must use the Git-backed environment manifest and D601 registry artifact consumer, not local-manifest target-side source build",
    })),
    policy: "prod deploy must not silently fall back to a dirty worktree, local manifest, target-side source build, or maintenance-channel deployment",
  };
}

export function prodArtifactLiveApplyBlockedResult(services: DeployManifestService[]): Record<string, unknown> {
  return {
    ok: false,
    supported: true,
    error: "live-prod-apply-blocked",
    services: services.map((service) => ({
      id: service.id,
      repo: service.repo,
      commitId: service.commitId,
      supported: true,
      liveApplyAllowed: false,
      reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? "live production artifact apply is blocked by policy",
    })),
    policy: "prod dry-run/plan is available, but worker automation must not run live production apply for these services",
    dryRunCommandShape: "bun scripts/cli.ts deploy apply --env prod --service <id> --dry-run",
  };
}

export function devArtifactLiveApplyBlockedResult(services: DeployManifestService[]): Record<string, unknown> {
  return {
    ok: false,
    supported: true,
    error: "live-dev-apply-blocked",
    services: services.map((service) => ({
      id: service.id,
      repo: service.repo,
      commitId: service.commitId,
      supported: true,
      liveApplyAllowed: false,
      requiresSupervisorApproval: true,
      reason: devArtifactLiveApplyBlockedServiceIds.get(service.id) ?? "live DEV artifact apply is blocked by policy",
      selfBootstrapGuard: service.id === "code-queue" ? codeQueueSelfBootstrapGuard("dev") : undefined,
    })),
    policy: "dev dry-run/plan is available, but a running Code Queue task must not perform live DEV Code Queue apply without explicit supervisor or human authorization outside Code Queue",
    dryRunCommandShape: "bun scripts/cli.ts deploy apply --env dev --service <id> --dry-run",
  };
}
