// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
// Main-server Docker lifecycle helpers with bounded status probes and job summaries.
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import { commandOk, runCommand, tailFile } from "./command";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { startJob } from "./jobs";
import { swapStatus } from "./swap";

export interface ComposeRuntimeEnv {
  envFile: string;
  logDir: string;
  logDay: string;
  logPrefix: string;
}

export interface ContainerStatus {
  id: string;
  name: string;
  image: string;
  status: string;
  ports: string;
}

export interface DeprecatedHostComposeEntry {
  ok: false;
  error: "deprecated-host-compose-entry";
  runnerDisposition: "business-failed";
  hostRole: "d601-provider-host";
  composeProject: string;
  cwd: string;
  message: string;
  decision: {
    hostComposeRetained: false;
    disposition: "deprecated";
    reason: string;
  };
  evidence: {
    containers: ContainerStatus[];
    publicPorts: Array<{ name: string; port: number; listening: boolean }>;
    conflictingListeners: Array<{ name: string; port: number; listening: boolean; expected: string }>;
    logDir: string;
  };
  correctEntrypoints: Record<string, string>;
  references: string[];
}

const rebuildableServices = ["backend-core", "frontend", "dev-frontend-proxy", "provider-gateway", "project-manager", "baidu-netdisk", "oa-event-flow", "code-queue-mgr"] as const;
const restartableServices = [...rebuildableServices, "pk01-postgres-relay"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export type RestartableService = typeof restartableServices[number];

export function isRebuildableService(value: string | undefined): value is RebuildableService {
  return rebuildableServices.some((service) => service === value);
}

export function isRestartableService(value: string | undefined): value is RestartableService {
  return restartableServices.some((service) => service === value);
}

export function unsupportedRebuildService(value: string | undefined): Record<string, unknown> {
  const service = value ?? null;
  const classifications: Record<string, Record<string, unknown>> = {
    database: {
      classification: "upstream-digest",
      reason: "database uses the upstream postgres:16-alpine image and a named PGDATA volume; it is not a source-built UniDesk service",
      replacement: "use server start/stop for lifecycle only; database image pin/mirror policy belongs to the upstream image precheck",
      deleteAllowedLater: false,
    },
    filebrowser: {
      classification: "upstream-digest",
      reason: "filebrowser is an upstream image consumer pinned by digest/mirror policy, not a Dockerfile source-build service",
      replacement: "future pull-only upstream digest CD; current provider-local docker-run repair remains maintenance-only",
      deleteAllowedLater: false,
    },
    "filebrowser-d601": {
      classification: "upstream-digest",
      reason: "filebrowser-d601 is an upstream image consumer pinned by digest/mirror policy, not a Dockerfile source-build service",
      replacement: "future pull-only upstream digest CD; current provider-local docker-run repair remains maintenance-only",
      deleteAllowedLater: false,
    },
    "code-queue": {
      classification: "standard-artifact",
      reason: "code-queue execution plane is native-k3s-managed and only supports the dev artifact consumer in this phase",
      replacement: "deploy apply --env dev --service code-queue or artifact-registry deploy-service --env dev --service code-queue",
      deleteAllowedLater: false,
    },
    "k3sctl-adapter": {
      classification: "bootstrap-keep",
      reason: "k3sctl-adapter is a D601 control bridge outside the main-server Compose stack",
      replacement: "keep direct bridge repair and artifact plan/dry-run; live prod apply requires supervisor confirmation",
      deleteAllowedLater: false,
    },
    "pk01-postgres-relay": {
      classification: "image-reuse",
      reason: "pk01-postgres-relay reuses the reviewed provider-gateway Bun image and has no source build step of its own",
      replacement: "server restart pk01-postgres-relay",
      deleteAllowedLater: false,
    },
  };
  return {
    ok: false,
    supported: false,
    error: "unsupported-server-rebuild",
    service,
    ...(typeof value === "string" && classifications[value] !== undefined ? classifications[value] : {
      classification: "needs-manual",
      reason: "server rebuild can mutate only reviewed main-server Docker Compose services",
      replacement: "inspect config.json, deploy.json, and docs/reference/cicd-standardization.md before choosing a release or maintenance path",
      deleteAllowedLater: false,
    }),
    allowedServices: [...rebuildableServices],
    policy: "server rebuild is a bootstrap/maintenance entrypoint and must not silently fall back to source builds for upstream images, D601 services, database, or unknown objects",
  };
}

export function unsupportedRestartService(value: string | undefined): Record<string, unknown> {
  const base = unsupportedRebuildService(value);
  return {
    ...base,
    error: "unsupported-server-restart",
    reason: typeof base.reason === "string" ? base.reason.replace(/server rebuild/g, "server restart") : base.reason,
    replacement: typeof base.replacement === "string" ? base.replacement.replace(/server rebuild/g, "server restart") : base.replacement,
    allowedServices: [...restartableServices],
    policy: "server restart is a no-build maintenance entrypoint and must not silently mutate upstream images, D601 services, database, or unknown objects",
  };
}

export function resolveComposeCommand(config: UniDeskConfig, envFile: string): string[] {
  const composeFile = rootPath(config.docker.composeFile);
  if (commandOk(["docker", "compose", "version"], repoRoot)) {
    return ["docker", "compose", "--env-file", envFile, "-f", composeFile, "-p", config.docker.projectName];
  }
  if (commandOk(["docker-compose", "--version"], repoRoot)) {
    return ["docker-compose", "--env-file", envFile, "-f", composeFile, "-p", config.docker.projectName];
  }
  throw new Error("Neither docker compose plugin nor docker-compose binary is available");
}

function pad(value: number): string {
  return String(value).padStart(2, "0");
}

function localDateParts(date: Date): { day: string; stamp: string } {
  const day = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`;
  const stamp = `${day}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
  return { day, stamp };
}

function envValue(value: string): string {
  if (/^[A-Za-z0-9_./:@-]+$/.test(value)) return value;
  return JSON.stringify(value);
}

function composeRuntimeEnvPreview(config: UniDeskConfig): ComposeRuntimeEnv {
  const parts = localDateParts(new Date());
  return {
    envFile: join(rootPath(config.paths.stateDir), "docker-compose.env"),
    logDir: "not-created-on-deprecated-d601-host-compose-entry",
    logDay: parts.day,
    logPrefix: parts.stamp,
  };
}

export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): ComposeRuntimeEnv {
  const stateDir = rootPath(config.paths.stateDir);
  mkdirSync(stateDir, { recursive: true });
  const envFile = join(stateDir, "docker-compose.env");
  const previousRaw = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
  const previousValue = (key: string): string => previousRaw.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]?.replace(/^"|"$/g, "") ?? "";
  const runtimeSecret = (key: string): string => process.env[key] ?? previousValue(key);
  const runtimeSecretWithDefault = (key: string, defaultValue: string, legacyDefault = ""): string => {
    if (process.env[key] !== undefined) return process.env[key] || defaultValue;
    const previous = previousValue(key);
    if (previous.length > 0 && previous !== legacyDefault) return previous;
    return defaultValue;
  };
  const logRoot = resolve(rootPath(config.paths.logsDir));
  let logDay: string;
  let logPrefix: string;
  if (!freshLogPrefix && previousRaw.length > 0) {
    logPrefix = previousValue("UNIDESK_LOG_PREFIX") || localDateParts(new Date()).stamp;
    logDay = previousValue("UNIDESK_LOG_DAY") || logPrefix.match(/^\d{8}/u)?.[0] || localDateParts(new Date()).day;
  } else {
    const parts = localDateParts(new Date());
    logDay = parts.day;
    logPrefix = parts.stamp;
  }
  mkdirSync(join(logRoot, logDay), { recursive: true });
  chmodSync(logRoot, 0o777);
  chmodSync(join(logRoot, logDay), 0o777);
  const labels = JSON.stringify(config.providerGateway.labels);
  const microservices = JSON.stringify(config.microservices);
  const restrictedHostBind = config.network.restrictedHostAccess?.bindHost || "127.0.0.1";
  const codeQueueService = config.microservices.find((service) => service.id === "code-queue");
  const codeQueueProviderId = codeQueueService?.providerId || "D601";
  const codeQueueRemoteWorkdir = codeQueueService?.development.worktreePath || "/home/ubuntu";
  const codeQueueExecutionProviderIds = codeQueueService?.deployment.expectedNodeIds?.join(",") || codeQueueProviderId;
  const codeQueueEnvValue = (key: string, defaultValue: string): string => {
    const value = process.env[key];
    return value === undefined || value.length === 0 ? defaultValue : value;
  };
  const lines = {
    UNIDESK_PUBLIC_HOST: config.network.publicHost,
    UNIDESK_CORE_PORT: String(config.network.core.port),
    UNIDESK_FRONTEND_PORT: String(config.network.frontend.port),
    UNIDESK_DEV_FRONTEND_PORT: String(config.network.devFrontend.port),
    UNIDESK_DATABASE_PORT: String(config.network.database.port),
    UNIDESK_DATABASE_BIND_HOST: runtimeSecretWithDefault("UNIDESK_DATABASE_BIND_HOST", restrictedHostBind, "127.0.0.1"),
    UNIDESK_PROVIDER_INGRESS_PORT: String(config.network.providerIngress.port),
    UNIDESK_PROVIDER_DATA_PORT: String(config.network.providerData.port),
    UNIDESK_PROVIDER_DATA_POOL_SIZE: runtimeSecret("UNIDESK_PROVIDER_DATA_POOL_SIZE") || "10",
    UNIDESK_PROVIDER_DATA_CONNECT_TIMEOUT_MS: runtimeSecret("UNIDESK_PROVIDER_DATA_CONNECT_TIMEOUT_MS") || "5000",
    UNIDESK_BACKEND_CORE_CARGO_BUILD_JOBS: runtimeSecret("UNIDESK_BACKEND_CORE_CARGO_BUILD_JOBS") || "1",
    UNIDESK_DATABASE_USER: config.database.user,
    UNIDESK_DATABASE_PASSWORD: config.database.password,
    UNIDESK_DATABASE_NAME: config.database.name,
    UNIDESK_DATABASE_VOLUME: config.database.volume,
    UNIDESK_DATABASE_VOLUME_SIZE: config.database.volumeSize,
    UNIDESK_PROVIDER_TOKEN: config.providerGateway.token,
    UNIDESK_SSH_CLIENT_TOKEN: runtimeSecret("UNIDESK_SSH_CLIENT_TOKEN"),
    UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: runtimeSecret("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST") || "G14,G14:*,D601,D601:*",
    UNIDESK_PROVIDER_ID: config.providerGateway.id,
    UNIDESK_PROVIDER_NAME: config.providerGateway.name,
    UNIDESK_PROVIDER_LABELS_JSON: labels,
    UNIDESK_MICROSERVICES_JSON: microservices,
    UNIDESK_DEPLOY_REF: runtimeSecret("UNIDESK_DEPLOY_REF"),
    UNIDESK_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_DEPLOY_SERVICE_ID") || "backend-core",
    UNIDESK_DEPLOY_REPO: runtimeSecret("UNIDESK_DEPLOY_REPO"),
    UNIDESK_DEPLOY_COMMIT: runtimeSecret("UNIDESK_DEPLOY_COMMIT"),
    UNIDESK_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_DEPLOY_REQUESTED_COMMIT"),
    UNIDESK_FRONTEND_DEPLOY_REF: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REF"),
    UNIDESK_FRONTEND_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_SERVICE_ID") || "frontend",
    UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"),
    UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_COMMIT"),
    UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT"),
    UNIDESK_PROJECT_MANAGER_DEPLOY_REF: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REF"),
    UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID") || "project-manager",
    UNIDESK_PROJECT_MANAGER_DEPLOY_REPO: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REPO"),
    UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT"),
    UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT"),
    UNIDESK_OA_EVENT_FLOW_DEPLOY_REF: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REF"),
    UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID") || "oa-event-flow",
    UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO"),
    UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT"),
    UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT"),
    UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF"),
    UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID") || "code-queue-mgr",
    UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO"),
    UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT"),
    UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT"),
    UNIDESK_AUTH_BROKER_DEPLOY_REF: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_REF"),
    UNIDESK_AUTH_BROKER_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_SERVICE_ID") || "auth-broker",
    UNIDESK_AUTH_BROKER_DEPLOY_REPO: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_REPO"),
    UNIDESK_AUTH_BROKER_DEPLOY_COMMIT: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_COMMIT"),
    UNIDESK_AUTH_BROKER_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_AUTH_BROKER_DEPLOY_REQUESTED_COMMIT"),
    UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED: runtimeSecret("UNIDESK_AUTH_BROKER_GITHUB_CONFIGURED") || "false",
    UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF: runtimeSecret("UNIDESK_AUTH_BROKER_GITHUB_CREDENTIAL_REF") || "github:unidesk-dev",
    UNIDESK_AUTH_BROKER_ALLOWED_REPOS: runtimeSecret("UNIDESK_AUTH_BROKER_ALLOWED_REPOS") || "pikasTech/unidesk",
    UNIDESK_AUTH_USERNAME: config.auth.username,
    UNIDESK_AUTH_PASSWORD: config.auth.password,
    UNIDESK_SESSION_SECRET: config.auth.sessionSecret,
    UNIDESK_SESSION_TTL_SECONDS: String(config.auth.sessionTtlSeconds),
    UNIDESK_HEARTBEAT_INTERVAL_MS: String(config.providerGateway.heartbeatIntervalMs),
    UNIDESK_HEARTBEAT_TIMEOUT_MS: "90000",
    UNIDESK_TASK_PENDING_TIMEOUT_MS: "600000",
    UNIDESK_RECONNECT_BASE_MS: String(config.providerGateway.reconnectBaseMs),
    UNIDESK_RECONNECT_MAX_MS: String(config.providerGateway.reconnectMaxMs),
    UNIDESK_MONITOR_DISK_PATH: config.providerGateway.metrics.diskPath,
    UNIDESK_PROVIDER_UPGRADE_HOST_PROJECT_ROOT: config.providerGateway.upgrade.hostProjectRoot,
    UNIDESK_PROVIDER_UPGRADE_WORKSPACE_PATH: config.providerGateway.upgrade.workspacePath,
    UNIDESK_PROVIDER_UPGRADE_COMPOSE_FILE: config.providerGateway.upgrade.composeFile,
    UNIDESK_PROVIDER_UPGRADE_ENV_FILE: config.providerGateway.upgrade.composeEnvFile,
    UNIDESK_PROVIDER_UPGRADE_COMPOSE_PROJECT: config.providerGateway.upgrade.composeProject,
    UNIDESK_PROVIDER_UPGRADE_SERVICE: config.providerGateway.upgrade.service,
    UNIDESK_PROVIDER_UPGRADE_RUNNER_IMAGE: config.providerGateway.upgrade.runnerImage,
    UNIDESK_LOG_DIR: logRoot,
    UNIDESK_LOG_DAY: logDay,
    UNIDESK_LOG_PREFIX: logPrefix,
    UNIDESK_LOG_RETENTION_BYTES: runtimeSecretWithDefault("UNIDESK_LOG_RETENTION_BYTES", "512MiB", "1GiB"),
    UNIDESK_DOCKER_LOG_MAX_SIZE: runtimeSecret("UNIDESK_DOCKER_LOG_MAX_SIZE") || "20m",
    UNIDESK_DOCKER_LOG_MAX_FILE: runtimeSecret("UNIDESK_DOCKER_LOG_MAX_FILE") || "3",
    UNIDESK_HOST_ROOT_SSH_DIR: process.env.UNIDESK_HOST_ROOT_SSH_DIR || "/root/.ssh",
    UNIDESK_HOST_SSH_KEY_DIR: config.sshForwarding.keyDir,
    UNIDESK_HOST_SSH_HOST: config.sshForwarding.host,
    UNIDESK_HOST_SSH_PORT: String(config.sshForwarding.port),
    UNIDESK_HOST_SSH_USER: config.sshForwarding.user,
    UNIDESK_CODE_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_KEY") || runtimeSecret("MINIMAX_API_KEY"),
    UNIDESK_CODE_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_MODEL") || runtimeSecret("MINIMAX_MODEL") || "MiniMax-M2.7",
    UNIDESK_CODE_QUEUE_MINIMAX_M3_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_M3_MODEL") || runtimeSecret("MINIMAX_M3_MODEL") || "MiniMax-M3",
    UNIDESK_CODE_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_BASE") || runtimeSecret("MINIMAX_API_BASE") || "https://api.minimaxi.com/v1",
    UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecretWithDefault("UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS", "90000", "60000"),
    UNIDESK_CODE_QUEUE_REMOTE_WORKDIR: codeQueueEnvValue("UNIDESK_CODE_QUEUE_REMOTE_WORKDIR", codeQueueRemoteWorkdir),
    UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID: codeQueueEnvValue("UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID", codeQueueProviderId),
    UNIDESK_CODE_QUEUE_TRACE_DATABASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_URL")
      || `postgres://${config.database.user}:${config.database.password}@database:${config.network.database.containerPort}/${config.database.name}`,
    UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX") || "2",
    UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX") || "1",
    UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS: codeQueueEnvValue("UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS", codeQueueExecutionProviderIds),
    UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: codeQueueEnvValue("UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID", codeQueueService?.development.providerId || codeQueueProviderId),
    UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE"),
    UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR: codeQueueEnvValue("UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR", codeQueueRemoteWorkdir),
    UNIDESK_OA_EVENT_FLOW_BASE_URL: runtimeSecret("UNIDESK_OA_EVENT_FLOW_BASE_URL") || "http://oa-event-flow:4255",
    UNIDESK_OA_EVENT_FLOW_PORT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_PORT") || "4255",
    UNIDESK_OA_EVENT_FLOW_BIND_HOST: runtimeSecretWithDefault("UNIDESK_OA_EVENT_FLOW_BIND_HOST", restrictedHostBind, "127.0.0.1"),
    UNIDESK_PK01_POSTGRES_RELAY_LISTEN_HOST: runtimeSecret("UNIDESK_PK01_POSTGRES_RELAY_LISTEN_HOST") || "0.0.0.0",
    UNIDESK_PK01_POSTGRES_RELAY_LISTEN_PORT: runtimeSecret("UNIDESK_PK01_POSTGRES_RELAY_LISTEN_PORT") || "15433",
    UNIDESK_PK01_POSTGRES_RELAY_TARGET_HOST: runtimeSecret("UNIDESK_PK01_POSTGRES_RELAY_TARGET_HOST") || "82.156.23.220",
    UNIDESK_PK01_POSTGRES_RELAY_TARGET_PORT: runtimeSecret("UNIDESK_PK01_POSTGRES_RELAY_TARGET_PORT") || "5432",
    UNIDESK_PK01_POSTGRES_RELAY_IDLE_TIMEOUT_MS: runtimeSecret("UNIDESK_PK01_POSTGRES_RELAY_IDLE_TIMEOUT_MS") || "300000",
    UNIDESK_PK01_POSTGRES_RELAY_ALLOWED_SOURCE_CIDRS: runtimeSecret("UNIDESK_PK01_POSTGRES_RELAY_ALLOWED_SOURCE_CIDRS") || "127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://claudeqq.unidesk.svc.cluster.local:3290",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"),
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS") || "12000",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS") || "15000",
    UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS") || "3",
    UNIDESK_BAIDU_NETDISK_CLIENT_ID: runtimeSecret("UNIDESK_BAIDU_NETDISK_CLIENT_ID"),
    UNIDESK_BAIDU_NETDISK_CLIENT_SECRET: runtimeSecret("UNIDESK_BAIDU_NETDISK_CLIENT_SECRET"),
    UNIDESK_BAIDU_NETDISK_TOKEN_KEY: runtimeSecret("UNIDESK_BAIDU_NETDISK_TOKEN_KEY"),
    UNIDESK_BAIDU_NETDISK_APP_ROOT: runtimeSecret("UNIDESK_BAIDU_NETDISK_APP_ROOT") || "/apps/UniDeskBaiduNetdisk",
    OPENAI_API_KEY: runtimeSecret("OPENAI_API_KEY"),
    CRS_OAI_KEY: runtimeSecret("CRS_OAI_KEY"),
  };
  writeFileSync(envFile, Object.entries(lines).map(([key, value]) => `${key}=${envValue(value)}`).join("\n") + "\n", "utf8");
  return { envFile, logDir: logRoot, logDay, logPrefix };
}

export function composeConfig(config: UniDeskConfig): { runtimeEnv: ComposeRuntimeEnv; command: string[]; result: ReturnType<typeof runCommand> } {
  const runtimeEnv = writeComposeEnv(config, false);
  const compose = resolveComposeCommand(config, runtimeEnv.envFile);
  const command = [...compose, "config", "-q"];
  return { runtimeEnv, command, result: runCommand(command, repoRoot) };
}

export function startStack(config: UniDeskConfig): unknown {
  const containers = dockerContainers(config);
  const deprecatedEntry = deprecatedHostComposeEntry(config, composeRuntimeEnvPreview(config), containers);
  if (deprecatedEntry !== null) return deprecatedEntry;
  const runtimeEnv = writeComposeEnv(config, true);
  const compose = resolveComposeCommand(config, runtimeEnv.envFile);
  const occupiedPorts = fixedPorts(config).filter((item) => item.listening);
  if (occupiedPorts.length > 0 && containers.length === 0) {
    throw new Error(`Fixed UniDesk port is occupied before start: ${occupiedPorts.map((p) => `${p.name}:${p.port}`).join(", ")}`);
  }
  const upCommand = [...compose, "up", "-d", "--build", "--remove-orphans"];
  const script = [
    "set -euo pipefail",
    restrictedHostAccessScript(config),
    shellJoin(upCommand),
  ].filter((line) => line.length > 0).join("\n");
  const command = ["bash", "-lc", composeLockedScript(script)];
  const job = startJob("server_start", command, "Build and start UniDesk services without tearing down running queue containers");
  return { job, runtimeEnv, command, ports: fixedPorts(config) };
}

export function stopStack(config: UniDeskConfig): unknown {
  const runtimeEnv = writeComposeEnv(config, false);
  const compose = resolveComposeCommand(config, runtimeEnv.envFile);
  const downCommand = [...compose, "down", "--remove-orphans"];
  const command = ["bash", "-lc", composeLockedScript(`set -euo pipefail; ${shellJoin(downCommand)}`)];
  const job = startJob("server_stop", command, "Stop all UniDesk Docker services managed by the fixed compose project");
  return { job, runtimeEnv, command, portsBeforeStop: fixedPorts(config) };
}

export function rebuildService(config: UniDeskConfig, service: RebuildableService): unknown {
  const runtimeEnv = writeComposeEnv(config, false);
  const compose = resolveComposeCommand(config, runtimeEnv.envFile);
  const buildCommand = [...compose, "build", service];
  const listServiceContainersCommand = [
    "docker",
    "ps",
    "-q",
    "--filter",
    `label=com.docker.compose.project=${config.docker.projectName}`,
    "--filter",
    `label=com.docker.compose.service=${service}`,
    "--filter",
    "label=com.docker.compose.oneoff=False",
  ];
  const upCommand = [...compose, "up", "-d", "--no-deps", "--force-recreate", service];
  const restoreCommand = [...compose, "up", "-d", "--no-deps", service];
  const listAllServiceContainersCommand = [...listServiceContainersCommand];
  listAllServiceContainersCommand[2] = "-a";
  const lockPath = composeLockPath();
  const watchdogLog = rootPath(".state", "jobs", "compose-rebuild-watchdog.log");
  const watchdogInnerScript = [
    "set -euo pipefail",
    "sleep 20",
    `cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
    `if [ -z "$cid" ]; then echo "$(date -Is) compose_rebuild_watchdog_restore service=${service}" >> ${shellQuote(watchdogLog)}; ${shellJoin(restoreCommand)} >> ${shellQuote(watchdogLog)} 2>&1 || true; fi`,
  ].join("\n");
  const watchdogScript = `set -euo pipefail; ${shellJoin(["flock", "-w", "30", lockPath, "bash", "-lc", watchdogInnerScript])} || true`;
  const validateScript = [
    "ready=0",
    "for attempt in $(seq 1 60); do",
    `cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
    "if [ -n \"$cid\" ]; then",
    "health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' $cid 2>/dev/null | head -1 || true)",
    `echo "service_container_probe service=${service} attempt=$attempt cid=$cid health=$health"`,
    "if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
    "else",
    `echo "service_container_probe service=${service} attempt=$attempt cid=missing"`,
    "fi",
    "sleep 1",
    "done",
    "if [ \"$ready\" != \"1\" ]; then",
    `echo "service_container_not_ready service=${service}" >&2`,
    `${shellJoin(listAllServiceContainersCommand)} --format '{{.ID}} {{.Names}} {{.Status}}' >&2 || true`,
    "exit 1",
    "fi",
  ].join("\n");
  const script = [
    "set -euo pipefail",
    `echo ${shellJoin(["rebuild_service", service, "build_first_then_force_recreate_with_validation"])}`,
    restrictedHostAccessScript(config),
    shellJoin(buildCommand),
    `nohup bash -lc ${shellQuote(watchdogScript)} >/dev/null 2>&1 &`,
    shellJoin(upCommand),
    validateScript,
  ].join("\n");
  const command = ["bash", "-lc", composeLockedScript(script)];
  const job = startJob("server_rebuild", command, `Rebuild and validate UniDesk ${service} with serialized Docker Compose mutation`);
  return {
    job,
    runtimeEnv,
    service,
    command,
    strategy: {
      buildBeforeReplace: true,
      replaceScope: {
        projectLabel: config.docker.projectName,
        serviceLabel: service,
      },
      noDeps: true,
      forceRecreate: true,
      composeMutationLock: rootPath(".state", "locks", "server-compose.lock"),
      jobRunner: "local",
      postUpValidation: true,
      namedVolumesPreserved: true,
    },
  };
}

export function restartService(config: UniDeskConfig, service: RestartableService): unknown {
  const runtimeEnv = writeComposeEnv(config, false);
  const compose = resolveComposeCommand(config, runtimeEnv.envFile);
  const listServiceContainersCommand = [
    "docker",
    "ps",
    "-q",
    "--filter",
    `label=com.docker.compose.project=${config.docker.projectName}`,
    "--filter",
    `label=com.docker.compose.service=${service}`,
    "--filter",
    "label=com.docker.compose.oneoff=False",
  ];
  const upCommand = [...compose, "up", "-d", "--no-build", "--no-deps", "--force-recreate", service];
  const listAllServiceContainersCommand = [...listServiceContainersCommand];
  listAllServiceContainersCommand[2] = "-a";
  const validateScript = [
    "ready=0",
    "for attempt in $(seq 1 60); do",
    `cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
    "if [ -n \"$cid\" ]; then",
    "health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' $cid 2>/dev/null | head -1 || true)",
    `echo "service_container_probe service=${service} attempt=$attempt cid=$cid health=$health"`,
    "if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
    "else",
    `echo "service_container_probe service=${service} attempt=$attempt cid=missing"`,
    "fi",
    "sleep 1",
    "done",
    "if [ \"$ready\" != \"1\" ]; then",
    `echo "service_container_not_ready service=${service}" >&2`,
    `${shellJoin(listAllServiceContainersCommand)} --format '{{.ID}} {{.Names}} {{.Status}}' >&2 || true`,
    "exit 1",
    "fi",
  ].join("\n");
  const script = [
    "set -euo pipefail",
    `echo ${shellJoin(["restart_service", service, "no_build_force_recreate_with_validation"])}`,
    restrictedHostAccessScript(config),
    shellJoin(upCommand),
    validateScript,
  ].join("\n");
  const command = ["bash", "-lc", composeLockedScript(script)];
  const job = startJob("server_restart", command, `Restart and validate UniDesk ${service} without building images`);
  return {
    job,
    runtimeEnv,
    service,
    command,
    strategy: {
      buildBeforeReplace: false,
      noBuild: true,
      replaceScope: {
        projectLabel: config.docker.projectName,
        serviceLabel: service,
      },
      noDeps: true,
      forceRecreate: true,
      composeMutationLock: rootPath(".state", "locks", "server-compose.lock"),
      jobRunner: "local",
      postUpValidation: true,
      namedVolumesPreserved: true,
    },
  };
}

function fixedPorts(config: UniDeskConfig): Array<{ name: string; port: number; listening: boolean }> {
  return [
    { name: "frontend", port: config.network.frontend.port, listening: isPortListening(config.network.frontend.port) },
    { name: "dev-frontend", port: config.network.devFrontend.port, listening: isPortListening(config.network.devFrontend.port) },
    { name: "provider-ingress", port: config.network.providerIngress.port, listening: isPortListening(config.network.providerIngress.port) },
    { name: "provider-data", port: config.network.providerData.port, listening: isPortListening(config.network.providerData.port) },
  ];
}

function restrictedHostAccessScript(config: UniDeskConfig): string {
  const access = config.network.restrictedHostAccess;
  if (access === undefined || access.bindHost === "127.0.0.1") return "";
  const ports = [
    { name: "database", containerPort: config.network.database.containerPort },
    { name: "oa-event-flow", containerPort: 4255 },
  ];
  return [
    "iptables -N DOCKER-USER 2>/dev/null || true",
    ...access.allowedForwardRules.map((rule) => [
      `iptables -C DOCKER-USER -s ${shellQuote(rule.sourceCidr)} -d ${shellQuote(rule.destinationCidr)} -p tcp --dport ${rule.port} -j ACCEPT 2>/dev/null`,
      `  || iptables -I DOCKER-USER 1 -s ${shellQuote(rule.sourceCidr)} -d ${shellQuote(rule.destinationCidr)} -p tcp --dport ${rule.port} -j ACCEPT`,
      `echo ${shellJoin(["restricted_host_forward_access", rule.id, rule.sourceCidr, rule.destinationCidr, String(rule.port), rule.purpose])}`,
    ].join(" \\\n")),
    ...ports.flatMap((port) => [
      ...access.allowedSourceCidrs.map((source) => [
        `iptables -C DOCKER-USER -s ${shellQuote(source)} -p tcp --dport ${port.containerPort} -j ACCEPT 2>/dev/null`,
        `  || iptables -I DOCKER-USER 1 -s ${shellQuote(source)} -p tcp --dport ${port.containerPort} -j ACCEPT`,
      ].join(" \\\n")),
      [
        `iptables -C DOCKER-USER -p tcp --dport ${port.containerPort} -j DROP 2>/dev/null \\`,
        "  || {",
        "    return_line=$(iptables -L DOCKER-USER --line-numbers | awk '$2==\"RETURN\" {print $1; exit}')",
        "    if [ -n \"$return_line\" ]; then",
        `      iptables -I DOCKER-USER "$return_line" -p tcp --dport ${port.containerPort} -j DROP`,
        "    else",
        `      iptables -A DOCKER-USER -p tcp --dport ${port.containerPort} -j DROP`,
        "    fi",
        "  }",
      ].join("\n"),
      `echo ${shellJoin(["restricted_host_access", port.name, String(port.containerPort), access.allowedSourceCidrs.join(",")])}`,
    ]),
  ].join("\n");
}

function shellJoin(args: string[]): string {
  return args.map(shellQuote).join(" ");
}

function shellQuote(arg: string): string {
  return `'${arg.replace(/'/g, `'\\''`)}'`;
}

function composeLockedScript(innerScript: string): string {
  const lockPath = composeLockPath();
  return [
    "set -euo pipefail",
    `mkdir -p ${shellQuote(rootPath(".state", "locks"))}`,
    `echo ${shellJoin(["compose_lock_wait", lockPath])}`,
    shellJoin(["flock", lockPath, "bash", "-lc", innerScript]),
  ].join("; ");
}

function composeLockPath(): string {
  const lockDir = rootPath(".state", "locks");
  mkdirSync(lockDir, { recursive: true });
  return join(lockDir, "server-compose.lock");
}

function isPortListening(port: number): boolean {
  const result = runCommand(["ss", "-ltn"], repoRoot);
  if (result.exitCode !== 0) return false;
  return result.stdout.split("\n").some((line) => line.includes(`:${port} `) || line.includes(`:${port}\t`));
}

export function dockerContainers(config: UniDeskConfig): ContainerStatus[] {
  const result = runCommand([
    "docker",
    "ps",
    "-a",
    "--filter",
    `label=com.docker.compose.project=${config.docker.projectName}`,
    "--filter",
    "label=com.docker.compose.oneoff=False",
    "--format",
    "{{json .}}",
  ], repoRoot, { timeoutMs: 5000 });
  if (result.exitCode !== 0 || result.stdout.trim().length === 0) return [];
  return result.stdout
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => JSON.parse(line) as Record<string, string>)
    .map((row) => ({ id: row.ID ?? "", name: row.Names ?? "", image: row.Image ?? "", status: row.Status ?? "", ports: row.Ports ?? "" }));
}

async function probe(url: string): Promise<unknown> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 2500);
  try {
    const res = await fetch(url, { signal: controller.signal });
    const body = await res.text();
    return { ok: res.ok, status: res.status, body: body.slice(0, 1200) };
  } catch (error) {
    return { ok: false, error: error instanceof Error ? error.message : String(error) };
  } finally {
    clearTimeout(timer);
  }
}

function dockerExecJson(container: string, path: string): unknown {
  const url = `http://127.0.0.1:8080${path}`;
  const script = [
    "set -eu",
    "if command -v backend-core >/dev/null 2>&1; then",
    `  exec backend-core --fetch-json ${shellQuote(url)}`,
    "fi",
    `url=${shellQuote(url)}`,
    "export url",
    "bun -e 'const url=process.env.url; fetch(url).then(async r=>{const text=await r.text(); console.log(JSON.stringify({ok:r.ok,status:r.status,body:text?JSON.parse(text):null})); process.exit(r.ok?0:1);}).catch(e=>{console.error(e); process.exit(1)})'",
  ].join("\n");
  const result = runCommand(["docker", "exec", container, "sh", "-lc", script], repoRoot, { timeoutMs: 5000 });
  if (result.exitCode !== 0) {
    return { ok: false, exitCode: result.exitCode, timedOut: result.timedOut, signal: result.signal, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
  }
  try {
    return JSON.parse(result.stdout.trim()) as unknown;
  } catch {
    return { ok: true, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
  }
}

function dockerExec(config: UniDeskConfig, container: string, command: string[]): unknown {
  const result = runCommand(["docker", "exec", container, ...command], repoRoot, { timeoutMs: 5000 });
  return { ok: result.exitCode === 0, exitCode: result.exitCode, timedOut: result.timedOut, signal: result.signal, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
}

export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
  const containers = dockerContainers(config);
  const deprecatedEntry = deprecatedHostComposeEntry(config, composeRuntimeEnvPreview(config), containers);
  if (deprecatedEntry !== null) return deprecatedEntry;
  const runtimeEnv = writeComposeEnv(config, false);
  const runtimeRaw = existsSync(runtimeEnv.envFile) ? readFileSync(runtimeEnv.envFile, "utf8") : "";
  const runtimeValue = (key: string): string => runtimeRaw.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]?.replace(/^"|"$/g, "") ?? "";
  const databaseBindHost = runtimeValue("UNIDESK_DATABASE_BIND_HOST") || "127.0.0.1";
  const oaEventFlowBindHost = runtimeValue("UNIDESK_OA_EVENT_FLOW_BIND_HOST") || "127.0.0.1";
  const oaEventFlowPort = Number(runtimeValue("UNIDESK_OA_EVENT_FLOW_PORT") || "4255");
  const pk01PostgresRelayListenHost = runtimeValue("UNIDESK_PK01_POSTGRES_RELAY_LISTEN_HOST") || "0.0.0.0";
  const pk01PostgresRelayPort = Number(runtimeValue("UNIDESK_PK01_POSTGRES_RELAY_LISTEN_PORT") || "15433");
  const coreHealth = dockerExecJson("unidesk-backend-core", "/health");
  const overview = dockerExecJson("unidesk-backend-core", "/api/overview");
  return {
    runtimeEnv,
    swap: swapStatus(),
    publicPorts: fixedPorts(config),
    blockedPublicPorts: [
      { name: "backend-core-rest", port: config.network.core.port, listening: isPortListening(config.network.core.port), expected: "not-listening" },
    ],
    restrictedHostPorts: [
      {
        name: "database",
        bindHost: databaseBindHost,
        hostPort: config.network.database.port,
        containerPort: config.network.database.containerPort,
        listening: isPortListening(config.network.database.port),
        expected: databaseBindHost === "127.0.0.1" ? "local-only" : "restricted-to-code-queue-provider",
      },
      {
        name: "oa-event-flow",
        bindHost: oaEventFlowBindHost,
        hostPort: oaEventFlowPort,
        containerPort: 4255,
        listening: isPortListening(oaEventFlowPort),
        expected: oaEventFlowBindHost === "127.0.0.1" ? "local-only" : "restricted-to-code-queue-provider",
      },
      {
        name: "pk01-postgres-relay",
        bindHost: pk01PostgresRelayListenHost,
        hostPort: pk01PostgresRelayPort,
        containerPort: pk01PostgresRelayPort,
        listening: isPortListening(pk01PostgresRelayPort),
        expected: "source-acl-private-or-local-only",
      },
    ],
    internalPorts: [
      { name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null },
      { name: "dev-frontend-proxy", containerPort: config.network.devFrontend.containerPort, hostPort: config.network.devFrontend.port },
      { name: "database", containerPort: config.network.database.containerPort, hostPort: null },
      { name: "project-manager", containerPort: 4233, hostPort: null },
      { name: "baidu-netdisk", containerPort: 4244, hostPort: null },
      { name: "oa-event-flow", containerPort: 4255, hostPort: null },
      { name: "pk01-postgres-relay", containerPort: pk01PostgresRelayPort, hostPort: pk01PostgresRelayPort },
    ],
    containers,
    health: {
      core: coreHealth,
      frontend: await probe(`http://127.0.0.1:${config.network.frontend.port}/health`),
      devFrontend: await probe(`http://127.0.0.1:${config.network.devFrontend.port}/health`),
      providerIngress: await probe(`http://127.0.0.1:${config.network.providerIngress.port}/health`),
      providerDataPortListening: isPortListening(config.network.providerData.port),
      database: dockerExec(config, "unidesk-database", ["pg_isready", "-U", config.database.user, "-d", config.database.name]),
      pk01PostgresRelay: dockerExec(config, "unidesk-pk01-postgres-relay", ["bun", "/workspace/scripts/runtime/pk01-postgres-relay.js", "--healthcheck"]),
      overview,
    },
    urls: {
      frontend: `http://${config.network.publicHost}:${config.network.frontend.port}`,
      devFrontend: `http://${config.network.publicHost}:${config.network.devFrontend.port}`,
      providerIngress: `ws://${config.network.publicHost}:${config.network.providerIngress.port}/ws/provider`,
      providerData: `tcp://${config.network.publicHost}:${config.network.providerData.port}`,
      internalCore: `http://backend-core:${config.network.core.containerPort}`,
      internalDatabase: `postgres://${config.database.user}:***@database:${config.network.database.containerPort}/${config.database.name}`,
    },
  };
}

export function deprecatedHostComposeEntryForTest(config: UniDeskConfig, runtimeEnv: ComposeRuntimeEnv, containers: ContainerStatus[], currentRoot: string): DeprecatedHostComposeEntry | null {
  return deprecatedHostComposeEntry(config, runtimeEnv, containers, currentRoot);
}

function deprecatedHostComposeEntry(config: UniDeskConfig, runtimeEnv: ComposeRuntimeEnv, containers: ContainerStatus[], currentRoot = repoRoot): DeprecatedHostComposeEntry | null {
  const cwd = resolve(currentRoot);
  const legacyHostWorkspaces = ["/home/ubuntu/unidesk", "/home/ubuntu/workspace/unidesk-dev", "/workspace/unidesk"];
  const cwdLooksLegacyHost = legacyHostWorkspaces.some((workspace) => cwd === workspace || cwd.startsWith(`${workspace}/.worktree/`));
  const upgradeRootLooksMainServer = config.providerGateway.upgrade.hostProjectRoot === "/root/unidesk";
  if (!cwdLooksLegacyHost || !upgradeRootLooksMainServer || containers.length > 0) return null;
  const publicPorts = fixedPorts(config);
  const conflictingListeners = publicPorts
    .filter((item) => item.listening)
    .map((item) => ({
      name: item.name,
      port: item.port,
      listening: item.listening,
      expected: "ignored-on-deprecated-d601-host-compose-entry",
    }));
  return {
    ok: false,
    error: "deprecated-host-compose-entry",
    runnerDisposition: "business-failed",
    hostRole: "d601-provider-host",
    composeProject: config.docker.projectName,
    cwd,
    message: "D601 host Compose is not the UniDesk server acceptance path. This command is deprecated here and does not start or validate the main-server stack.",
    decision: {
      hostComposeRetained: false,
      disposition: "deprecated",
      reason: "D601 owns provider-local Docker services and native k3s workloads; main-server Compose remains rooted at /root/unidesk on the main server.",
    },
    evidence: {
      containers,
      publicPorts,
      conflictingListeners,
      logDir: runtimeEnv.logDir,
    },
    correctEntrypoints: {
      mainServerStatus: "run bun scripts/cli.ts server status from /root/unidesk on the main server",
      d601NativeK3s: "trans D601:k3s kubectl get deploy,svc,pod,endpoints -n unidesk -o wide",
      decisionCenter: "bun scripts/cli.ts microservice health decision-center",
      d601DevDeploy: "bun scripts/cli.ts deploy plan --env dev --service <service>",
    },
    references: [
      "docs/reference/deployment.md#docker-compose-runtime",
      "docs/reference/dev-environment.md#d601-unidesk-workspace",
      "docs/reference/deploy.md#d601-native-k3s-emergency-guard",
    ],
  };
}

function listLogFiles(root: string): string[] {
  if (!existsSync(root)) return [];
  const entries = readdirSync(root, { withFileTypes: true });
  const files: string[] = [];
  for (const entry of entries) {
    const path = join(root, entry.name);
    if (entry.isDirectory()) files.push(...listLogFiles(path));
    if (entry.isFile()) files.push(path);
  }
  return files.sort();
}

export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
  const logRoot = rootPath(config.paths.logsDir);
  const runtimeEnv = writeComposeEnv(config, false);
  const allFiles = listLogFiles(logRoot);
  const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix));
  const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12);
  const files = selectedFiles.map((path) => {
    const sizeBytes = existsSync(path) ? statSync(path).size : 0;
    const truncated = sizeBytes > tailBytes;
    return { path, name: basename(path), sizeBytes, tailBytes, truncated, tail: tailFile(path, tailBytes) };
  });
  const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-dev-frontend-proxy", "unidesk-provider-gateway-main", "unidesk-pk01-postgres-relay", "project-manager-backend", "baidu-netdisk-backend", "oa-event-flow-backend"];
  const docker = containerNames.map((name) => {
    const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
    return {
      name,
      exitCode: result.exitCode,
      tailBytes,
      stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
      stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
      stdoutTruncated: Buffer.byteLength(result.stdout, "utf8") > tailBytes,
      stderrTruncated: Buffer.byteLength(result.stderr, "utf8") > tailBytes,
      stdoutTail: result.stdout.slice(-tailBytes),
      stderrTail: result.stderr.slice(-tailBytes),
    };
  });
  return {
    logRoot,
    runtimeEnv,
    policy: {
      defaultTailBytes: 3000,
      requestedTailBytes: tailBytes,
      selectedFileLimit: 12,
      dockerTailLines: 40,
      disclosure: "server logs returns tails only; increase with --tail-bytes for a larger bounded tail, and inspect listed paths directly for full logs.",
    },
    files,
    docker,
  };
}
