import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { isAbsolute } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import { startJob } from "./jobs";
import {
  applyPk01CaddyBlock,
  capture,
  compactCapture,
  dryRunManifestScript,
  parseJsonOutput,
  prepareFrpcSecret,
  publicHttpProbe,
  publicServicePolicyChecks,
  redactText,
  renderFrpcManifest,
  shQuote,
  type FrpcSecretMaterial,
} from "./platform-infra-public-service";
import {
  createYamlFieldReader,
} from "./platform-infra-ops-library";
import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets";

const configFile = rootPath("config", "platform-infra", "langbot.yaml");
const configLabel = "config/platform-infra/langbot.yaml";
const serviceName = "langbot";
const pluginRuntimeServiceName = "langbot-plugin-runtime";
const fieldManager = "unidesk-platform-langbot";
const {
  asRecord,
  objectField,
  arrayOfRecords,
  stringField,
  integerField,
  booleanField,
  stringArrayField,
  numberArrayField,
  enumField,
  kubernetesNameField,
  sourceRefField,
  envKeyField,
  pgIdentifierField,
  hostField,
  portField,
  absolutePathField,
  apiPathField,
  httpsUrlField,
} = createYamlFieldReader(configLabel);

interface LangBotConfig {
  version: number;
  kind: "platform-infra-langbot";
  metadata: { id: string; owner: string; relatedIssues: number[] };
  defaults: { targetId: string };
  image: { repository: string; tag: string; pullPolicy: "Always" | "IfNotPresent" | "Never" };
  dependencyImages: { postgresClient: string };
  targets: LangBotTarget[];
  runtime: {
    timezone: string;
    database: {
      sourceRef: string;
      sourceKeys: { user: string; password: string; dbName: string };
      secretName: string;
      host: string;
      port: number;
      user: string;
      dbName: string;
      sslMode: string;
      appDatabaseValue: string;
    };
    secrets: { root: string; appSourceRef: string };
    storage: {
      data: PvcSpec;
      plugins: PvcSpec;
      pluginRuntime: PvcSpec;
    };
    box: { enabled: boolean; reason: string };
  };
  apiKey: { sourceRef: string; key: string; name: string; description: string };
  officialWechat: {
    supportedAdapters: string[];
    webhookBaseUrl: string;
    defaultCallbackPath: string;
    notes: string[];
  };
}

interface PvcSpec {
  name: string;
  size: string;
}

interface LangBotTarget {
  id: string;
  route: string;
  namespace: string;
  enabled: boolean;
  replicas: number;
  publicExposure: PublicExposure;
}

interface PublicExposure {
  enabled: boolean;
  publicBaseUrl: string;
  dns: { hostname: string; expectedA: string; resolvers: string[] };
  frpc: {
    deploymentName: string;
    secretName: string;
    secretKey: string;
    image: string;
    serverAddr: string;
    serverPort: number;
    proxyName: string;
    remotePort: number;
    localIP: string;
    localPort: number;
    tokenSourceRef: string;
    tokenSourceKey: string;
  };
  pk01: {
    route: string;
    caddyConfigPath: string;
    caddyServiceName: string;
    responseHeaderTimeoutSeconds: number;
  };
}

interface CommonOptions {
  targetId: string | null;
  full: boolean;
  raw: boolean;
}

interface ApplyOptions extends CommonOptions {
  confirm: boolean;
  dryRun: boolean;
  wait: boolean;
}

interface QueryOptions extends CommonOptions {
  path: string;
}

interface LogsOptions extends CommonOptions {
  component: "app" | "plugin-runtime" | "frpc" | "all";
  lines: number;
}

interface SecretMaterial {
  dbSourceRef: string;
  dbSourcePath: string;
  appSourceRef: string;
  appSourcePath: string;
  action: "read";
  values: {
    dbUser: string;
    dbPassword: string;
    dbName: string;
    jwtSecret: string;
    apiKey: string;
  };
  fingerprint: string;
  valuesPrinted: false;
}

export function langBotHelp(): Record<string, unknown> {
  return {
    command: "platform-infra langbot plan|apply|status|logs|validate|bootstrap-api-key|query",
    output: "json",
    usage: [
      "bun scripts/cli.ts platform-infra langbot plan [--target G14]",
      "bun scripts/cli.ts platform-infra langbot apply [--target G14] --dry-run",
      "bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm",
      "bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra langbot logs [--target G14] [--component app|plugin-runtime|frpc|all] [--lines N]",
      "bun scripts/cli.ts platform-infra langbot validate [--target G14] [--full|--raw]",
      "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm",
      "bun scripts/cli.ts platform-infra langbot query --path /api/v1/platform/bots",
    ],
    configTruth: "config/platform-infra/langbot.yaml",
    publicUrl: "https://langbot.pikapython.com",
    secretPolicy: "API keys, database passwords and JWT secrets are never printed; output uses presence, key names and fingerprints only.",
  };
}

export async function runLangBotCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
  const [action = "plan"] = args;
  if (action === "plan") return plan(parseCommonOptions(args.slice(1)));
  if (action === "apply") return await apply(config, parseApplyOptions(args.slice(1)));
  if (action === "status") return await status(config, parseCommonOptions(args.slice(1)));
  if (action === "logs") return await logs(config, parseLogsOptions(args.slice(1)));
  if (action === "validate") return await validate(config, parseCommonOptions(args.slice(1)));
  if (action === "bootstrap-api-key") return bootstrapApiKey(parseApplyOptions(args.slice(1)));
  if (action === "query") return query(parseQueryOptions(args.slice(1)));
  return { ok: false, error: "unsupported-platform-infra-langbot-command", args, help: langBotHelp() };
}

function parseCommonOptions(args: string[]): CommonOptions {
  let targetId: string | null = null;
  let full = false;
  let raw = false;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--target") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
      targetId = value;
      index += 1;
    } else if (arg === "--full") {
      full = true;
    } else if (arg === "--raw") {
      raw = true;
      full = true;
    } else {
      throw new Error(`unsupported langbot option: ${arg}`);
    }
  }
  if (targetId !== null && !/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error("--target must be a simple target id");
  return { targetId, full, raw };
}

function parseApplyOptions(args: string[]): ApplyOptions {
  const commonArgs = args.filter((arg, index) => {
    const previous = args[index - 1];
    return arg !== "--confirm" && arg !== "--dry-run" && arg !== "--wait" && previous !== "--component" && previous !== "--lines" && previous !== "--path";
  });
  const common = parseCommonOptions(commonArgs);
  const allowedArgs = ["--target", "--confirm", "--dry-run", "--wait", ...(common.targetId === null ? [] : [common.targetId])];
  if (args.some((arg) => !allowedArgs.includes(arg) && !arg.startsWith("--target="))) {
    for (let index = 0; index < args.length; index += 1) {
      const arg = args[index];
      if (arg === "--target") {
        index += 1;
        continue;
      }
      if (arg === "--confirm" || arg === "--dry-run" || arg === "--wait") continue;
      throw new Error(`unsupported langbot apply option: ${arg}`);
    }
  }
  const confirm = args.includes("--confirm");
  const dryRun = args.includes("--dry-run") || !confirm;
  if (confirm && args.includes("--dry-run")) throw new Error("langbot apply accepts only one of --confirm or --dry-run");
  return { ...common, confirm, dryRun, wait: args.includes("--wait") };
}

function parseLogsOptions(args: string[]): LogsOptions {
  const commonArgs: string[] = [];
  let component: LogsOptions["component"] = "all";
  let lines = 120;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--component") {
      const value = args[index + 1];
      if (value !== "app" && value !== "plugin-runtime" && value !== "frpc" && value !== "all") throw new Error("--component must be app, plugin-runtime, frpc, or all");
      component = value;
      index += 1;
    } else if (arg === "--lines") {
      const value = Number(args[index + 1]);
      if (!Number.isInteger(value) || value < 1 || value > 500) throw new Error("--lines must be an integer in 1..500");
      lines = value;
      index += 1;
    } else {
      commonArgs.push(arg);
      if (arg === "--target") {
        commonArgs.push(args[index + 1] ?? "");
        index += 1;
      }
    }
  }
  return { ...parseCommonOptions(commonArgs), component, lines };
}

function parseQueryOptions(args: string[]): QueryOptions {
  const commonArgs: string[] = [];
  let path = "/api/v1/platform/bots";
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--path") {
      const value = args[index + 1];
      if (value === undefined || !value.startsWith("/") || value.includes("..")) throw new Error("--path must be an absolute API path without ..");
      path = value;
      index += 1;
    } else {
      commonArgs.push(arg);
      if (arg === "--target") {
        commonArgs.push(args[index + 1] ?? "");
        index += 1;
      }
    }
  }
  return { ...parseCommonOptions(commonArgs), path };
}

function readLangBotConfig(): LangBotConfig {
  const parsed = Bun.YAML.parse(readFileSync(configFile, "utf8")) as unknown;
  const root = asRecord(parsed, "config/platform-infra/langbot.yaml");
  const version = integerField(root, "version", "");
  const kind = stringField(root, "kind", "");
  if (kind !== "platform-infra-langbot") throw new Error("config/platform-infra/langbot.yaml.kind must be platform-infra-langbot");
  const metadata = objectField(root, "metadata", "");
  const defaults = objectField(root, "defaults", "");
  const image = objectField(root, "image", "");
  const dependencyImages = objectField(root, "dependencyImages", "");
  const runtime = objectField(root, "runtime", "");
  const database = objectField(runtime, "database", "runtime");
  const sourceKeys = objectField(database, "sourceKeys", "runtime.database");
  const secrets = objectField(runtime, "secrets", "runtime");
  const storage = objectField(runtime, "storage", "runtime");
  const box = objectField(runtime, "box", "runtime");
  const apiKey = objectField(root, "apiKey", "");
  const officialWechat = objectField(root, "officialWechat", "");
  const config: LangBotConfig = {
    version,
    kind,
    metadata: {
      id: stringField(metadata, "id", "metadata"),
      owner: stringField(metadata, "owner", "metadata"),
      relatedIssues: numberArrayField(metadata, "relatedIssues", "metadata"),
    },
    defaults: {
      targetId: stringField(defaults, "targetId", "defaults"),
    },
    image: {
      repository: stringField(image, "repository", "image"),
      tag: stringField(image, "tag", "image"),
      pullPolicy: enumField(image, "pullPolicy", "image", ["Always", "IfNotPresent", "Never"] as const),
    },
    dependencyImages: {
      postgresClient: stringField(dependencyImages, "postgresClient", "dependencyImages"),
    },
    targets: arrayOfRecords(root.targets, "targets").map(parseTarget),
    runtime: {
      timezone: stringField(runtime, "timezone", "runtime"),
      database: {
        sourceRef: sourceRefField(database, "sourceRef", "runtime.database"),
        sourceKeys: {
          user: envKeyField(sourceKeys, "user", "runtime.database.sourceKeys"),
          password: envKeyField(sourceKeys, "password", "runtime.database.sourceKeys"),
          dbName: envKeyField(sourceKeys, "dbName", "runtime.database.sourceKeys"),
        },
        secretName: kubernetesNameField(database, "secretName", "runtime.database"),
        host: hostField(database, "host", "runtime.database"),
        port: portField(database, "port", "runtime.database"),
        user: pgIdentifierField(database, "user", "runtime.database"),
        dbName: pgIdentifierField(database, "dbName", "runtime.database"),
        sslMode: stringField(database, "sslMode", "runtime.database"),
        appDatabaseValue: stringField(database, "appDatabaseValue", "runtime.database"),
      },
      secrets: {
        root: stringField(secrets, "root", "runtime.secrets"),
        appSourceRef: sourceRefField(secrets, "appSourceRef", "runtime.secrets"),
      },
      storage: {
        data: pvcSpec(objectField(storage, "data", "runtime.storage"), "runtime.storage.data"),
        plugins: pvcSpec(objectField(storage, "plugins", "runtime.storage"), "runtime.storage.plugins"),
        pluginRuntime: pvcSpec(objectField(storage, "pluginRuntime", "runtime.storage"), "runtime.storage.pluginRuntime"),
      },
      box: {
        enabled: booleanField(box, "enabled", "runtime.box"),
        reason: stringField(box, "reason", "runtime.box"),
      },
    },
    apiKey: {
      sourceRef: sourceRefField(apiKey, "sourceRef", "apiKey"),
      key: envKeyField(apiKey, "key", "apiKey"),
      name: stringField(apiKey, "name", "apiKey"),
      description: stringField(apiKey, "description", "apiKey"),
    },
    officialWechat: {
      supportedAdapters: stringArrayField(officialWechat, "supportedAdapters", "officialWechat"),
      webhookBaseUrl: httpsUrlField(officialWechat, "webhookBaseUrl", "officialWechat"),
      defaultCallbackPath: apiPathField(officialWechat, "defaultCallbackPath", "officialWechat"),
      notes: stringArrayField(officialWechat, "notes", "officialWechat"),
    },
  };
  if (!isImageReference(`${config.image.repository}:${config.image.tag}`)) throw new Error("config/platform-infra/langbot.yaml.image must render a valid image reference");
  if (!isImageReference(config.dependencyImages.postgresClient)) throw new Error("config/platform-infra/langbot.yaml.dependencyImages.postgresClient must be an image reference");
  if (config.targets.length === 0) throw new Error("config/platform-infra/langbot.yaml.targets must not be empty");
  assertKnownEnabledTarget(config.targets, config.defaults.targetId, "defaults.targetId");
  return config;
}

function parseTarget(record: Record<string, unknown>, index: number): LangBotTarget {
  const path = `targets[${index}]`;
  const exposure = objectField(record, "publicExposure", path);
  const dns = objectField(exposure, "dns", `${path}.publicExposure`);
  const frpc = objectField(exposure, "frpc", `${path}.publicExposure`);
  const pk01 = objectField(exposure, "pk01", `${path}.publicExposure`);
  const publicBaseUrl = httpsUrlField(exposure, "publicBaseUrl", `${path}.publicExposure`);
  const hostname = hostField(dns, "hostname", `${path}.publicExposure.dns`);
  if (new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${path}.publicExposure hostname must match publicBaseUrl`);
  return {
    id: stringField(record, "id", path),
    route: stringField(record, "route", path),
    namespace: kubernetesNameField(record, "namespace", path),
    enabled: booleanField(record, "enabled", path),
    replicas: integerField(record, "replicas", path),
    publicExposure: {
      enabled: booleanField(exposure, "enabled", `${path}.publicExposure`),
      publicBaseUrl,
      dns: {
        hostname,
        expectedA: stringField(dns, "expectedA", `${path}.publicExposure.dns`),
        resolvers: stringArrayField(dns, "resolvers", `${path}.publicExposure.dns`),
      },
      frpc: {
        deploymentName: kubernetesNameField(frpc, "deploymentName", `${path}.publicExposure.frpc`),
        secretName: kubernetesNameField(frpc, "secretName", `${path}.publicExposure.frpc`),
        secretKey: stringField(frpc, "secretKey", `${path}.publicExposure.frpc`),
        image: stringField(frpc, "image", `${path}.publicExposure.frpc`),
        serverAddr: hostField(frpc, "serverAddr", `${path}.publicExposure.frpc`),
        serverPort: portField(frpc, "serverPort", `${path}.publicExposure.frpc`),
        proxyName: stringField(frpc, "proxyName", `${path}.publicExposure.frpc`),
        remotePort: portField(frpc, "remotePort", `${path}.publicExposure.frpc`),
        localIP: hostField(frpc, "localIP", `${path}.publicExposure.frpc`),
        localPort: portField(frpc, "localPort", `${path}.publicExposure.frpc`),
        tokenSourceRef: sourceRefField(frpc, "tokenSourceRef", `${path}.publicExposure.frpc`),
        tokenSourceKey: envKeyField(frpc, "tokenSourceKey", `${path}.publicExposure.frpc`),
      },
      pk01: {
        route: stringField(pk01, "route", `${path}.publicExposure.pk01`),
        caddyConfigPath: absolutePathField(pk01, "caddyConfigPath", `${path}.publicExposure.pk01`),
        caddyServiceName: stringField(pk01, "caddyServiceName", `${path}.publicExposure.pk01`),
        responseHeaderTimeoutSeconds: integerField(pk01, "responseHeaderTimeoutSeconds", `${path}.publicExposure.pk01`),
      },
    },
  };
}

function assertKnownEnabledTarget(targets: LangBotTarget[], targetId: string, path: string): void {
  const target = targets.find((item) => item.id.toLowerCase() === targetId.toLowerCase());
  if (target === undefined) throw new Error(`config/platform-infra/langbot.yaml.${path} references unknown target ${targetId}; known targets: ${targets.map((item) => item.id).join(", ")}`);
  if (!target.enabled) throw new Error(`config/platform-infra/langbot.yaml.${path} references disabled target ${target.id}`);
}

function resolveTarget(langbot: LangBotConfig, targetId: string | null): LangBotTarget {
  const resolvedTargetId = targetId ?? langbot.defaults.targetId;
  const target = langbot.targets.find((item) => item.id.toLowerCase() === resolvedTargetId.toLowerCase());
  if (target === undefined) throw new Error(`unknown LangBot target ${resolvedTargetId}; known targets: ${langbot.targets.map((item) => item.id).join(", ")}`);
  if (!target.enabled) throw new Error(`LangBot target ${target.id} is disabled in config/platform-infra/langbot.yaml`);
  return target;
}

function plan(options: CommonOptions): Record<string, unknown> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, options.targetId);
  const yaml = renderManifest(langbot, target);
  const policy = policyChecks(yaml, target);
  return {
    ok: policy.every((check) => check.ok),
    action: "platform-infra-langbot-plan",
    mutation: false,
    config: configSummary(langbot, target),
    policy,
    decision: {
      namespace: target.namespace,
      publicExposure: "PK01 Caddy -> PK01 frps remotePort -> G14 frpc -> LangBot ClusterIP",
      database: "PK01 existing host-native PostgreSQL instance; LangBot uses its own database and role in the same instance.",
      box: langbot.runtime.box.enabled ? "enabled" : `disabled: ${langbot.runtime.box.reason}`,
      officialWechat: langbot.officialWechat,
      resourcePolicy: "No Kubernetes CPU/memory requests or limits are rendered.",
    },
    next: {
      secrets: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm",
      dryRun: `bun scripts/cli.ts platform-infra langbot apply --target ${target.id} --dry-run`,
      apply: `bun scripts/cli.ts platform-infra langbot apply --target ${target.id} --confirm`,
      status: `bun scripts/cli.ts platform-infra langbot status --target ${target.id}`,
      validate: `bun scripts/cli.ts platform-infra langbot validate --target ${target.id}`,
    },
  };
}

async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, options.targetId);
  const yaml = renderManifest(langbot, target);
  const policy = policyChecks(yaml, target);
  if (!policy.every((check) => check.ok)) return { ok: false, action: "platform-infra-langbot-apply", mode: "policy-blocked", policy };
  if (options.confirm && !options.wait) {
    const job = startJob(
      `platform_infra_langbot_apply_${target.id.toLowerCase()}`,
      ["bun", "scripts/cli.ts", "platform-infra", "langbot", "apply", "--target", target.id, "--confirm", "--wait"],
      `Apply ${target.id} LangBot platform-infra manifests, FRP and PK01 Caddy exposure through the controlled UniDesk CLI`,
    );
    return {
      ok: true,
      action: "platform-infra-langbot-apply",
      mode: "async-job",
      mutation: true,
      target: targetSummary(target),
      job,
      statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
      next: {
        status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
        rollout: `bun scripts/cli.ts platform-infra langbot status --target ${target.id}`,
        validate: `bun scripts/cli.ts platform-infra langbot validate --target ${target.id}`,
      },
    };
  }
  if (options.dryRun) {
    const result = await capture(config, target.route, ["sh"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
    const parsed = parseJsonOutput(result.stdout);
    return {
      ok: result.exitCode === 0 && boolField(parsed, "ok", false),
      action: "platform-infra-langbot-apply",
      mode: "dry-run",
      mutation: false,
      target: targetSummary(target),
      policy,
      remote: parsed ?? compactCapture(result, { full: true }),
    };
  }
  const secretMaterial = prepareSecretMaterial(langbot);
  const frpcSecret = prepareFrpcSecret({
    secretRoot: secretRoot(langbot),
    exposure: target.publicExposure,
    sourcePathRedactor: redactRepoPath,
    parseEnvFile,
    requiredEnvValue,
    readTextFile,
  });
  const confirmedYaml = renderManifest(langbot, target, secretMaterial.fingerprint);
  const result = await capture(config, target.route, ["sh"], applyScript(confirmedYaml, langbot, target, secretMaterial, frpcSecret));
  const parsed = parseJsonOutput(result.stdout);
  const caddy = await applyPk01CaddyBlock(config, serviceName, target.publicExposure);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false) && caddy.ok === true,
    action: "platform-infra-langbot-apply",
    mode: "confirmed",
    mutation: true,
    target: targetSummary(target),
    policy,
    secrets: secretSummary(secretMaterial, frpcSecret),
    remote: parsed ?? compactCapture(result, { full: true }),
    pk01Caddy: caddy,
    next: {
      secrets: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm",
      status: `bun scripts/cli.ts platform-infra langbot status --target ${target.id}`,
      validate: `bun scripts/cli.ts platform-infra langbot validate --target ${target.id}`,
      bootstrapApiKey: "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm",
    },
  };
}

async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, options.targetId);
  const result = await capture(config, target.route, ["sh"], statusScript(langbot, target));
  const parsed = parseJsonOutput(result.stdout);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false),
    action: "platform-infra-langbot-status",
    target: targetSummary(target),
    summary: parsed,
    remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
    ...(options.raw ? { raw: result } : {}),
  };
}

async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record<string, unknown>> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, options.targetId);
  const result = await capture(config, target.route, ["sh"], logsScript(target, options));
  const parsed = parseJsonOutput(result.stdout);
  return {
    ok: result.exitCode === 0 && boolField(parsed, "ok", false),
    action: "platform-infra-langbot-logs",
    target: targetSummary(target),
    component: options.component,
    lines: options.lines,
    logs: parsed,
    remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
    ...(options.raw ? { raw: result } : {}),
  };
}

async function validate(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, options.targetId);
  const k8s = await capture(config, target.route, ["sh"], validateScript(target));
  const k8sParsed = parseJsonOutput(k8s.stdout);
  const publicProbe = publicHttpProbe(target.publicExposure.publicBaseUrl, "/api/v1/system/info");
  return {
    ok: k8s.exitCode === 0 && boolField(k8sParsed, "ok", false) && publicProbe.ok,
    action: "platform-infra-langbot-validate",
    target: targetSummary(target),
    k8s: k8sParsed ?? compactCapture(k8s, { full: true }),
    publicHttps: publicProbe,
    officialWechat: {
      webhookBaseUrl: langbot.officialWechat.webhookBaseUrl,
      callbackPath: langbot.officialWechat.defaultCallbackPath,
      supportedAdapters: langbot.officialWechat.supportedAdapters,
    },
    remote: compactCapture(k8s, { full: options.full || k8s.exitCode !== 0 }),
    ...(options.raw ? { raw: k8s } : {}),
  };
}

function bootstrapApiKey(options: ApplyOptions): Record<string, unknown> {
  const langbot = readLangBotConfig();
  resolveTarget(langbot, options.targetId);
  if (!options.confirm || options.dryRun) {
    return {
      ok: true,
      action: "platform-infra-langbot-bootstrap-api-key",
      mode: "dry-run",
      mutation: false,
      targetTable: "api_keys",
      next: { confirm: "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm" },
    };
  }
  const secret = prepareSecretMaterial(langbot);
  const conn = postgresConninfo(langbot, secret);
  const sql = [
    "INSERT INTO api_keys (name, key, description)",
    `VALUES (${sqlLiteral(langbot.apiKey.name)}, ${sqlLiteral(secret.values.apiKey)}, ${sqlLiteral(langbot.apiKey.description)})`,
    "ON CONFLICT (key) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, updated_at = now()",
    "RETURNING id, name, created_at, updated_at;",
  ].join("\n");
  const result = Bun.spawnSync([
    "psql",
    "-Atq",
    conn,
    "-c",
    sql,
  ], {
    stdout: "pipe",
    stderr: "pipe",
    env: { ...process.env, PGPASSWORD: secret.values.dbPassword },
  });
  const stdout = new TextDecoder().decode(result.stdout).trim();
  const stderr = new TextDecoder().decode(result.stderr).trim();
  const tableMissing = /relation "api_keys" does not exist/u.test(stderr);
  return {
    ok: result.exitCode === 0,
    action: "platform-infra-langbot-bootstrap-api-key",
    mode: "confirmed",
    mutation: true,
    apiKey: {
      name: langbot.apiKey.name,
      keySourceRef: langbot.apiKey.sourceRef,
      keyName: langbot.apiKey.key,
      fingerprint: fingerprintSecretValues({ [langbot.apiKey.key]: secret.values.apiKey }, [langbot.apiKey.key]),
      valuesPrinted: false,
    },
    database: {
      host: langbot.runtime.database.host,
      port: langbot.runtime.database.port,
      database: langbot.runtime.database.dbName,
      user: langbot.runtime.database.user,
      sslmode: langbot.runtime.database.sslMode,
    },
    result: {
      exitCode: result.exitCode,
      row: result.exitCode === 0 ? stdout : "",
      stderrTail: redactText(stderr).slice(-2000),
      blocker: tableMissing ? "api_keys-table-missing-run-langbot-first" : null,
    },
    next: tableMissing
      ? { waitForAppMigration: "bun scripts/cli.ts platform-infra langbot status --target G14", retry: "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm" }
      : { query: "bun scripts/cli.ts platform-infra langbot query --path /api/v1/platform/bots" },
  };
}

function query(options: QueryOptions): Record<string, unknown> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, options.targetId);
  const secret = prepareSecretMaterial(langbot);
  const probe = publicHttpProbe(target.publicExposure.publicBaseUrl, options.path, { headers: [`X-API-Key: ${secret.values.apiKey}`] });
  return {
    ok: probe.ok,
    action: "platform-infra-langbot-query",
    mutation: false,
    target: targetSummary(target),
    request: {
      method: "GET",
      baseUrl: target.publicExposure.publicBaseUrl,
      path: options.path,
      auth: "X-API-Key",
      apiKeyFingerprint: fingerprintSecretValues({ [langbot.apiKey.key]: secret.values.apiKey }, [langbot.apiKey.key]),
      valuesPrinted: false,
    },
    response: probe,
  };
}

function renderManifest(langbot: LangBotConfig, target: LangBotTarget, secretFingerprint = "not-prepared"): string {
  const image = `${langbot.image.repository}:${langbot.image.tag}`;
  const configHash = createHash("sha256").update(JSON.stringify({ langbot, target })).digest("hex").slice(0, 16);
  const db = langbot.runtime.database;
  const storage = langbot.runtime.storage;
  const exposure = target.publicExposure;
  return `apiVersion: v1
kind: Namespace
metadata:
  name: ${target.namespace}
  labels:
    app.kubernetes.io/name: platform-infra
    app.kubernetes.io/managed-by: unidesk
    unidesk.ai/runtime-node: ${target.id}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: platform-infra
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - {}
  egress:
    - {}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ${storage.data.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: ${storage.data.size}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ${storage.plugins.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot
    app.kubernetes.io/component: plugins
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: ${storage.plugins.size}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ${storage.pluginRuntime.name}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot-plugin-runtime
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: ${storage.pluginRuntime.size}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: langbot-config
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
data:
  TZ: "${langbot.runtime.timezone}"
  API__HOST: "0.0.0.0"
  API__PORT: "5300"
  API__WEBHOOK_PREFIX: "${exposure.publicBaseUrl}"
  API__EXTRA_WEBHOOK_PREFIX: ""
  DATABASE__USE: "postgresql"
  DATABASE__POSTGRESQL__HOST: "${db.host}"
  DATABASE__POSTGRESQL__PORT: "${db.port}"
  DATABASE__POSTGRESQL__USER: "${db.user}"
  DATABASE__POSTGRESQL__DATABASE: "${db.appDatabaseValue}"
  PLUGIN__RUNTIME_WS_URL: "ws://${pluginRuntimeServiceName}:5400/control/ws"
  BOX__ENABLED: "${langbot.runtime.box.enabled}"
  SPACE__DISABLE_TELEMETRY: "true"
  SYSTEM__OUTBOUND_IPS: ""
---
apiVersion: v1
kind: Service
metadata:
  name: ${pluginRuntimeServiceName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot-plugin-runtime
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  selector:
    app.kubernetes.io/name: langbot-plugin-runtime
  ports:
    - name: websocket
      port: 5400
      targetPort: websocket
---
apiVersion: v1
kind: Service
metadata:
  name: ${serviceName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  selector:
    app.kubernetes.io/name: langbot
  ports:
    - name: http
      port: 5300
      targetPort: http
    - name: callback-2280
      port: 2280
      targetPort: callback-2280
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${pluginRuntimeServiceName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot-plugin-runtime
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: langbot-plugin-runtime
  template:
    metadata:
      labels:
        app.kubernetes.io/name: langbot-plugin-runtime
        app.kubernetes.io/part-of: platform-infra
      annotations:
        unidesk.ai/langbot-config-hash: "${configHash}"
    spec:
      securityContext:
        fsGroup: 1000
      containers:
        - name: plugin-runtime
          image: ${image}
          imagePullPolicy: ${langbot.image.pullPolicy}
          command:
            - uv
            - run
            - --no-sync
            - -m
            - langbot_plugin.cli.__init__
            - rt
          ports:
            - name: websocket
              containerPort: 5400
          envFrom:
            - configMapRef:
                name: langbot-config
          volumeMounts:
            - name: plugin-runtime-data
              mountPath: /app/data/plugins
      volumes:
        - name: plugin-runtime-data
          persistentVolumeClaim:
            claimName: ${storage.pluginRuntime.name}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${serviceName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: langbot
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
spec:
  replicas: ${target.replicas}
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app.kubernetes.io/name: langbot
  template:
    metadata:
      labels:
        app.kubernetes.io/name: langbot
        app.kubernetes.io/part-of: platform-infra
      annotations:
        unidesk.ai/langbot-config-hash: "${configHash}"
        unidesk.ai/langbot-secret-fingerprint: "${secretFingerprint}"
        unidesk.ai/public-base-url: "${exposure.publicBaseUrl}"
        unidesk.ai/box-enabled: "${langbot.runtime.box.enabled}"
    spec:
      securityContext:
        fsGroup: 1000
      initContainers:
        - name: wait-postgres
          image: ${langbot.dependencyImages.postgresClient}
          imagePullPolicy: IfNotPresent
          command:
            - sh
            - -c
            - until pg_isready -h ${db.host} -p ${db.port} -U ${db.user} -d ${db.dbName}; do sleep 2; done
      containers:
        - name: langbot
          image: ${image}
          imagePullPolicy: ${langbot.image.pullPolicy}
          ports:
            - name: http
              containerPort: 5300
            - name: callback-2280
              containerPort: 2280
          envFrom:
            - configMapRef:
                name: langbot-config
          env:
            - name: DATABASE__POSTGRESQL__PASSWORD
              valueFrom:
                secretKeyRef:
                  name: ${db.secretName}
                  key: DATABASE_PASSWORD
            - name: SYSTEM__JWT__SECRET
              valueFrom:
                secretKeyRef:
                  name: ${db.secretName}
                  key: SYSTEM_JWT_SECRET
          volumeMounts:
            - name: langbot-data
              mountPath: /app/data
            - name: langbot-plugins
              mountPath: /app/plugins
      volumes:
        - name: langbot-data
          persistentVolumeClaim:
            claimName: ${storage.data.name}
        - name: langbot-plugins
          persistentVolumeClaim:
            claimName: ${storage.plugins.name}
${renderFrpcManifest(target)}
`;
}

function policyChecks(yaml: string, target: LangBotTarget): Array<Record<string, unknown>> {
  return [
    ...publicServicePolicyChecks(yaml, target, "LangBot"),
    { name: "no-box-docker-socket", ok: !/docker\.sock|langbot-box/u.test(yaml), detail: "LangBot Box is disabled by default and Docker socket is not mounted." },
  ];
}

export function prepareLangBotSecretMaterial(): SecretMaterial {
  return prepareSecretMaterial(readLangBotConfig());
}

export function readLangBotPostgresConninfo(): string {
  const langbot = readLangBotConfig();
  return postgresConninfo(langbot, prepareSecretMaterial(langbot));
}

function prepareSecretMaterial(langbot: LangBotConfig): SecretMaterial {
  const root = secretRoot(langbot);
  const dbSource = readEnvSourceFile({
    root,
    sourceRef: langbot.runtime.database.sourceRef,
    missingMessage: (sourcePath) => `LangBot database source ${redactRepoPath(sourcePath)} is missing; run platform-db postgres apply --config config/platform-db/postgres-pk01.yaml --confirm first`,
  });
  const dbValues = dbSource.values;
  const dbUser = requiredEnvValue(dbValues, langbot.runtime.database.sourceKeys.user, langbot.runtime.database.sourceRef);
  const dbPassword = requiredEnvValue(dbValues, langbot.runtime.database.sourceKeys.password, langbot.runtime.database.sourceRef);
  const dbName = requiredEnvValue(dbValues, langbot.runtime.database.sourceKeys.dbName, langbot.runtime.database.sourceRef);
  if (dbUser !== langbot.runtime.database.user) throw new Error(`${langbot.runtime.database.sourceRef}.${langbot.runtime.database.sourceKeys.user} does not match langbot runtime.database.user`);
  if (dbName !== langbot.runtime.database.dbName) throw new Error(`${langbot.runtime.database.sourceRef}.${langbot.runtime.database.sourceKeys.dbName} does not match langbot runtime.database.dbName`);
  const appSource = readEnvSourceFile({
    root,
    sourceRef: langbot.runtime.secrets.appSourceRef,
    missingMessage: (sourcePath) => `LangBot app secret source ${redactRepoPath(sourcePath)} is missing; run bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope platform-infra --confirm first`,
  });
  const appValues = appSource.values;
  const jwtSecret = requiredEnvValue(appValues, "LANGBOT_JWT_SECRET", langbot.runtime.secrets.appSourceRef);
  const apiKey = requiredEnvValue(appValues, langbot.apiKey.key, langbot.apiKey.sourceRef);
  const values = {
    dbUser,
    dbPassword,
    dbName,
    jwtSecret,
    apiKey,
  };
  return {
    dbSourceRef: langbot.runtime.database.sourceRef,
    dbSourcePath: dbSource.sourcePathRedacted,
    appSourceRef: langbot.runtime.secrets.appSourceRef,
    appSourcePath: appSource.sourcePathRedacted,
    action: "read",
    values,
    fingerprint: fingerprintSecretValues({ dbPassword, jwtSecret: values.jwtSecret, apiKey: values.apiKey }, ["dbPassword", "jwtSecret", "apiKey"]),
    valuesPrinted: false,
  };
}

export function readLangBotRuntimeConfig(): Record<string, unknown> {
  const langbot = readLangBotConfig();
  const target = resolveTarget(langbot, null);
  return {
    publicBaseUrl: target.publicExposure.publicBaseUrl,
    expectedNamespace: target.namespace,
    apiKeyName: langbot.apiKey.key,
    valuesPrinted: false,
  };
}

export function readLangBotSecretMaterial(): Record<string, unknown> {
  const langbot = readLangBotConfig();
  const secret = prepareSecretMaterial(langbot);
  return {
    apiKey: secret.values.apiKey,
    apiKeyFingerprint: fingerprintSecretValues({ [langbot.apiKey.key]: secret.values.apiKey }, [langbot.apiKey.key]),
    sourceRef: langbot.apiKey.sourceRef,
    keyName: langbot.apiKey.key,
    valuesPrinted: false,
  };
}

function applyScript(yaml: string, langbot: LangBotConfig, target: LangBotTarget, secret: SecretMaterial, frpc: FrpcSecretMaterial): string {
  const encodedManifest = Buffer.from(yaml, "utf8").toString("base64");
  const dbPasswordB64 = Buffer.from(secret.values.dbPassword, "utf8").toString("base64");
  const jwtB64 = Buffer.from(secret.values.jwtSecret, "utf8").toString("base64");
  const frpcB64 = Buffer.from(frpc.frpcToml, "utf8").toString("base64");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/langbot.k8s.yaml"
printf '%s' '${encodedManifest}' | base64 -d > "$manifest"
printf '%s' '${dbPasswordB64}' | base64 -d > "$tmp/db-password"
printf '%s' '${jwtB64}' | base64 -d > "$tmp/jwt-secret"
printf '%s' '${frpcB64}' | base64 -d > "$tmp/frpc.toml"
kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$tmp/ns.out" 2>"$tmp/ns.err"
ns_rc=$?
secret_rc=1
frpc_rc=1
apply_rc=1
if [ "$ns_rc" -eq 0 ]; then
  kubectl -n ${target.namespace} create secret generic ${langbot.runtime.database.secretName} \\
    --from-file=DATABASE_PASSWORD="$tmp/db-password" \\
    --from-file=SYSTEM_JWT_SECRET="$tmp/jwt-secret" \\
    --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$tmp/secret.out" 2>"$tmp/secret.err"
  secret_rc=$?
  kubectl -n ${target.namespace} create secret generic ${frpc.secretName} \\
    --from-file=${frpc.secretKey}="$tmp/frpc.toml" \\
    --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$tmp/frpc-secret.out" 2>"$tmp/frpc-secret.err"
  frpc_rc=$?
fi
if [ "$ns_rc" -eq 0 ] && [ "$secret_rc" -eq 0 ] && [ "$frpc_rc" -eq 0 ]; then
  kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
  apply_rc=$?
else
  : >"$tmp/apply.out"
  printf '%s\\n' 'skipped because namespace or secret sync failed' >"$tmp/apply.err"
fi
python3 - "$ns_rc" "$secret_rc" "$frpc_rc" "$apply_rc" "$tmp/ns.out" "$tmp/ns.err" "$tmp/secret.out" "$tmp/secret.err" "$tmp/frpc-secret.out" "$tmp/frpc-secret.err" "$tmp/apply.out" "$tmp/apply.err" <<'PY'
import json, sys
ns_rc, secret_rc, frpc_rc, apply_rc = [int(value) for value in sys.argv[1:5]]
def text(path, limit=6000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""
payload = {
    "ok": ns_rc == 0 and secret_rc == 0 and frpc_rc == 0 and apply_rc == 0,
    "target": "${target.id}",
    "namespace": "${target.namespace}",
    "image": "${langbot.image.repository}:${langbot.image.tag}",
    "secret": {"name": "${langbot.runtime.database.secretName}", "keys": ["DATABASE_PASSWORD", "SYSTEM_JWT_SECRET"], "valuesPrinted": False},
    "frpcSecret": {"name": "${frpc.secretName}", "key": "${frpc.secretKey}", "fingerprint": "${frpc.fingerprint}", "valuesPrinted": False},
    "steps": {
        "namespace": {"exitCode": ns_rc, "stdout": text(sys.argv[5]), "stderr": text(sys.argv[6])},
        "secret": {"exitCode": secret_rc, "stdout": text(sys.argv[7]), "stderr": text(sys.argv[8])},
        "frpcSecret": {"exitCode": frpc_rc, "stdout": text(sys.argv[9]), "stderr": text(sys.argv[10])},
        "apply": {"exitCode": apply_rc, "stdout": text(sys.argv[11], 10000), "stderr": text(sys.argv[12])},
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

function statusScript(langbot: LangBotConfig, target: LangBotTarget): string {
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
capture() {
  name="$1"
  shift
  "$@" -o json >"$tmp/$name.json" 2>"$tmp/$name.err"
  printf '%s' "$?" >"$tmp/$name.rc"
}
capture ns kubectl get namespace ${target.namespace}
capture deployments kubectl -n ${target.namespace} get deployments -l app.kubernetes.io/part-of=platform-infra
capture pods kubectl -n ${target.namespace} get pods -l app.kubernetes.io/part-of=platform-infra
capture services kubectl -n ${target.namespace} get services -l app.kubernetes.io/part-of=platform-infra
capture pvc kubectl -n ${target.namespace} get pvc -l app.kubernetes.io/part-of=platform-infra
capture configmap kubectl -n ${target.namespace} get configmap langbot-config
capture secret kubectl -n ${target.namespace} get secret ${langbot.runtime.database.secretName}
capture frpcSecret kubectl -n ${target.namespace} get secret ${target.publicExposure.frpc.secretName}
capture networkpolicy kubectl -n ${target.namespace} get networkpolicy allow-all
capture ingress kubectl -n ${target.namespace} get ingress
capture quota kubectl -n ${target.namespace} get resourcequota
capture limitrange kubectl -n ${target.namespace} get limitrange
capture events kubectl -n ${target.namespace} get events --sort-by=.lastTimestamp
python3 - "$tmp" <<'PY'
import json, os, sys
tmp = sys.argv[1]
def rc(name):
    try:
        return int(open(os.path.join(tmp, f"{name}.rc"), encoding="utf-8").read() or "1")
    except FileNotFoundError:
        return 1
def load(name):
    try:
        return json.load(open(os.path.join(tmp, f"{name}.json"), encoding="utf-8"))
    except Exception:
        return None
deployments = load("deployments") or {"items": []}
pods = load("pods") or {"items": []}
services = load("services") or {"items": []}
pvc = load("pvc") or {"items": []}
events = load("events") or {"items": []}
def deployment_summary(name):
    for item in deployments.get("items", []):
        if item.get("metadata", {}).get("name") == name:
            status = item.get("status", {})
            spec = item.get("spec", {})
            return {
                "name": name,
                "replicas": spec.get("replicas", 0),
                "readyReplicas": status.get("readyReplicas", 0),
                "updatedReplicas": status.get("updatedReplicas", 0),
                "availableReplicas": status.get("availableReplicas", 0),
            }
    return {"name": name, "missing": True, "readyReplicas": 0, "replicas": 0}
langbot_deploy = deployment_summary("${serviceName}")
plugin_deploy = deployment_summary("${pluginRuntimeServiceName}")
frpc_deploy = deployment_summary("${target.publicExposure.frpc.deploymentName}")
deployment_ready = (
    not langbot_deploy.get("missing")
    and not plugin_deploy.get("missing")
    and not frpc_deploy.get("missing")
    and langbot_deploy.get("readyReplicas", 0) >= 1
    and plugin_deploy.get("readyReplicas", 0) >= 1
    and frpc_deploy.get("readyReplicas", 0) >= 1
)
watched_names = {"${serviceName}", "${pluginRuntimeServiceName}", "${target.publicExposure.frpc.deploymentName}"}
watched_pod_names = {
    item.get("metadata", {}).get("name")
    for item in pods.get("items", [])
    if item.get("metadata", {}).get("labels", {}).get("app.kubernetes.io/name") in watched_names
}
watched_pvc_names = {
    item.get("metadata", {}).get("name")
    for item in pvc.get("items", [])
    if item.get("metadata", {}).get("labels", {}).get("app.kubernetes.io/part-of") == "platform-infra"
       and str(item.get("metadata", {}).get("name", "")).startswith("langbot-")
}
def compact_message(value, limit=500):
    if not value:
        return None
    value = str(value).replace("\\n", " ")
    return value if len(value) <= limit else value[:limit] + "...(truncated)"
def state_summary(status):
    state = status.get("state") or {}
    if not state:
        return {"type": "unknown"}
    state_type = next(iter(state.keys()))
    detail = state.get(state_type) or {}
    return {
        "type": state_type,
        "reason": detail.get("reason"),
        "message": compact_message(detail.get("message")),
    }
def condition_summary(condition):
    return {
        "type": condition.get("type"),
        "status": condition.get("status"),
        "reason": condition.get("reason"),
        "message": compact_message(condition.get("message")),
    }
def event_summary(event):
    involved = event.get("involvedObject", {})
    return {
        "type": event.get("type"),
        "reason": event.get("reason"),
        "message": compact_message(event.get("message"), 700),
        "count": event.get("count"),
        "lastTimestamp": event.get("lastTimestamp") or event.get("eventTime"),
        "involvedObject": {
            "kind": involved.get("kind"),
            "name": involved.get("name"),
        },
    }
relevant_events = []
for event in events.get("items", []):
    involved = event.get("involvedObject", {})
    kind = involved.get("kind")
    name = involved.get("name")
    if (
        name in watched_pod_names
        or name in watched_pvc_names
        or name in watched_names
        or (kind == "ReplicaSet" and any(str(name or "").startswith(prefix + "-") for prefix in watched_names))
    ):
        relevant_events.append(event_summary(event))
payload = {
    "ok": rc("ns") == 0 and rc("deployments") == 0 and rc("pods") == 0 and deployment_ready,
    "namespace": "${target.namespace}",
    "publicBaseUrl": "${target.publicExposure.publicBaseUrl}",
    "database": {
        "host": "${langbot.runtime.database.host}",
        "database": "${langbot.runtime.database.dbName}",
        "user": "${langbot.runtime.database.user}",
        "sslMode": "${langbot.runtime.database.sslMode}",
    },
    "deployments": {
        "langbot": langbot_deploy,
        "pluginRuntime": plugin_deploy,
        "frpc": frpc_deploy,
    },
    "pods": [
        {
            "name": item.get("metadata", {}).get("name"),
            "phase": item.get("status", {}).get("phase"),
            "ready": sum(1 for condition in item.get("status", {}).get("conditions", []) if condition.get("type") == "Ready" and condition.get("status") == "True"),
            "conditions": [
                condition_summary(condition)
                for condition in item.get("status", {}).get("conditions", [])
                if condition.get("status") != "True" or condition.get("type") in ("Ready", "ContainersReady", "PodScheduled")
            ],
            "initContainers": [
                {
                    "name": c.get("name"),
                    "ready": c.get("ready"),
                    "restartCount": c.get("restartCount"),
                    "state": state_summary(c),
                }
                for c in item.get("status", {}).get("initContainerStatuses", [])
            ],
            "containers": [
                {
                    "name": c.get("name"),
                    "ready": c.get("ready"),
                    "restartCount": c.get("restartCount"),
                    "state": state_summary(c),
                }
                for c in item.get("status", {}).get("containerStatuses", [])
            ],
        }
        for item in pods.get("items", [])
        if item.get("metadata", {}).get("name") in watched_pod_names
    ],
    "events": relevant_events[-20:],
    "services": [item.get("metadata", {}).get("name") for item in services.get("items", [])],
    "pvcs": [item.get("metadata", {}).get("name") for item in pvc.get("items", [])],
    "secrets": {
        "app": {"name": "${langbot.runtime.database.secretName}", "exists": rc("secret") == 0, "valuesPrinted": False},
        "frpc": {"name": "${target.publicExposure.frpc.secretName}", "exists": rc("frpcSecret") == 0, "valuesPrinted": False},
    },
    "policyObjects": {
        "allowAllNetworkPolicy": rc("networkpolicy") == 0,
        "ingressCount": len((load("ingress") or {}).get("items", [])),
        "resourceQuotaCount": len((load("quota") or {}).get("items", [])),
        "limitRangeCount": len((load("limitrange") or {}).get("items", [])),
    },
    "officialWechat": {
        "webhookBaseUrl": "${langbot.officialWechat.webhookBaseUrl}",
        "defaultCallbackPath": "${langbot.officialWechat.defaultCallbackPath}",
        "supportedAdapters": ${JSON.stringify(langbot.officialWechat.supportedAdapters)},
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
`;
}

function logsScript(target: LangBotTarget, options: LogsOptions): string {
  const components = options.component === "all" ? ["app", "plugin-runtime", "frpc"] : [options.component];
  const commands = components.map((component) => {
    const deployment = component === "app" ? serviceName : component === "plugin-runtime" ? pluginRuntimeServiceName : target.publicExposure.frpc.deploymentName;
    return `kubectl -n ${target.namespace} logs deploy/${deployment} --tail=${options.lines} --all-containers=true >"$tmp/${component}.out" 2>"$tmp/${component}.err"; printf '%s' "$?" >"$tmp/${component}.rc"`;
  }).join("\n");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
${commands}
python3 - "$tmp" ${components.map((item) => shQuote(item)).join(" ")} <<'PY'
import json, os, re, sys
tmp = sys.argv[1]
components = sys.argv[2:]
payload = {"ok": True, "components": {}, "valuesPrinted": False}
secret_markers = ("PASSWORD", "SECRET", "TOKEN", "API_KEY", "DATABASE_URL")
generic_secret = re.compile(r"(?i)((?:password|secret|token|api[_-]?key|database_url)\\s*[=:]\\s*)[^\\s,;]+")
def redact(value):
    value = re.sub(r"lbk_[A-Za-z0-9_-]+", "lbk_<redacted>", value)
    value = re.sub(r"(postgresql://)[^@\\s]+@", r"\\1<redacted>@", value)
    redacted_lines = []
    for line in value.splitlines():
        if "env_key:" in line and "env_value:" in line:
            key = line.split("env_key:", 1)[1].split(",", 1)[0].strip().upper()
            if any(marker in key for marker in secret_markers):
                line = line.split("env_value:", 1)[0] + "env_value: <redacted>"
        line = generic_secret.sub(r"\\1<redacted>", line)
        redacted_lines.append(line)
    return "\\n".join(redacted_lines)
for component in components:
    def text(suffix):
        try:
            return redact(open(os.path.join(tmp, f"{component}.{suffix}"), encoding="utf-8", errors="replace").read())[-12000:]
        except FileNotFoundError:
            return ""
    rc_text = text("rc").strip()
    rc = int(rc_text or "1")
    payload["components"][component] = {"exitCode": rc, "stdoutTail": text("out"), "stderrTail": text("err")}
    if rc != 0:
        payload["ok"] = False
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
`;
}

function validateScript(target: LangBotTarget): string {
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
kubectl -n ${target.namespace} get deploy ${serviceName} -o json >"$tmp/deploy.json" 2>"$tmp/deploy.err"
deploy_rc=$?
kubectl -n ${target.namespace} get deploy ${target.publicExposure.frpc.deploymentName} -o json >"$tmp/frpc.json" 2>"$tmp/frpc.err"
frpc_rc=$?
kubectl -n ${target.namespace} exec deploy/${serviceName} -c langbot -- python -c 'import json, urllib.request
try:
    with urllib.request.urlopen("http://127.0.0.1:5300/api/v1/system/info", timeout=10) as resp:
        print(json.dumps({"ok": 200 <= resp.status < 500, "status": resp.status, "bytes": len(resp.read())}))
except Exception as exc:
    print(json.dumps({"ok": False, "error": str(exc)}))
    raise' >"$tmp/http.out" 2>"$tmp/http.err"
http_rc=$?
python3 - "$deploy_rc" "$frpc_rc" "$http_rc" "$tmp/deploy.json" "$tmp/deploy.err" "$tmp/frpc.json" "$tmp/frpc.err" "$tmp/http.out" "$tmp/http.err" <<'PY'
import json, sys
deploy_rc, frpc_rc, http_rc = [int(value) for value in sys.argv[1:4]]
def load(path):
    try:
        return json.load(open(path, encoding="utf-8"))
    except Exception:
        return None
def text(path):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-3000:]
    except FileNotFoundError:
        return ""
deploy = load(sys.argv[4]) or {}
frpc = load(sys.argv[6]) or {}
http = load(sys.argv[8]) or {}
payload = {
    "ok": deploy_rc == 0 and frpc_rc == 0 and http_rc == 0 and http.get("ok") is True,
    "deployments": {
        "langbotReady": deploy.get("status", {}).get("readyReplicas", 0),
        "frpcReady": frpc.get("status", {}).get("readyReplicas", 0),
    },
    "internalHttp": http,
    "errors": {
        "deploy": text(sys.argv[5]),
        "frpc": text(sys.argv[7]),
        "http": text(sys.argv[9]),
    },
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

function postgresConninfo(langbot: LangBotConfig, secret: SecretMaterial): string {
  const db = langbot.runtime.database;
  return `host=${db.host} port=${db.port} user=${db.user} dbname=${db.dbName} sslmode=${db.sslMode} connect_timeout=10`;
}

function configSummary(langbot: LangBotConfig, target: LangBotTarget): Record<string, unknown> {
  return {
    path: "config/platform-infra/langbot.yaml",
    metadata: langbot.metadata,
    target: targetSummary(target),
    image: `${langbot.image.repository}:${langbot.image.tag}`,
    pullPolicy: langbot.image.pullPolicy,
    database: {
      host: langbot.runtime.database.host,
      port: langbot.runtime.database.port,
      user: langbot.runtime.database.user,
      dbName: langbot.runtime.database.dbName,
      appDatabaseValue: langbot.runtime.database.appDatabaseValue,
      sslMode: langbot.runtime.database.sslMode,
      sourceRef: langbot.runtime.database.sourceRef,
      secretName: langbot.runtime.database.secretName,
      valuesPrinted: false,
    },
    storage: langbot.runtime.storage,
    publicExposure: {
      publicBaseUrl: target.publicExposure.publicBaseUrl,
      dns: target.publicExposure.dns,
      frpc: {
        deploymentName: target.publicExposure.frpc.deploymentName,
        remotePort: target.publicExposure.frpc.remotePort,
        localIP: target.publicExposure.frpc.localIP,
        localPort: target.publicExposure.frpc.localPort,
        tokenSourceRef: target.publicExposure.frpc.tokenSourceRef,
        valuesPrinted: false,
      },
      pk01: target.publicExposure.pk01,
    },
    apiKey: {
      sourceRef: langbot.apiKey.sourceRef,
      key: langbot.apiKey.key,
      name: langbot.apiKey.name,
      valuesPrinted: false,
    },
    officialWechat: langbot.officialWechat,
  };
}

function targetSummary(target: LangBotTarget): Record<string, unknown> {
  return { id: target.id, route: target.route, namespace: target.namespace, replicas: target.replicas };
}

function secretSummary(secret: SecretMaterial, frpc: FrpcSecretMaterial): Record<string, unknown> {
  return {
    dbSourceRef: secret.dbSourceRef,
    dbSourcePath: secret.dbSourcePath,
    appSourceRef: secret.appSourceRef,
    appSourcePath: secret.appSourcePath,
    action: secret.action,
    fingerprint: secret.fingerprint,
    frpc: {
      sourceRef: frpc.sourceRef,
      sourcePath: frpc.sourcePath,
      secretName: frpc.secretName,
      secretKey: frpc.secretKey,
      fingerprint: frpc.fingerprint,
    },
    valuesPrinted: false,
  };
}

function secretRoot(langbot: LangBotConfig): string {
  const root = langbot.runtime.secrets.root;
  return isAbsolute(root) ? root : rootPath(root);
}

function sqlLiteral(value: string): string {
  return `'${value.replaceAll("'", "''")}'`;
}

function boolField(value: Record<string, unknown> | null, key: string, fallback: boolean): boolean {
  return typeof value?.[key] === "boolean" ? value[key] : fallback;
}

function pvcSpec(record: Record<string, unknown>, path: string): PvcSpec {
  const name = kubernetesNameField(record, "name", path);
  const size = stringField(record, "size", path);
  if (!/^[0-9]+(Gi|Mi)$/u.test(size)) throw new Error(`config/platform-infra/langbot.yaml.${path}.size must use Mi or Gi`);
  return { name, size };
}

function isImageReference(value: string): boolean {
  return /^[A-Za-z0-9._:/-]+$/u.test(value) && value.includes(":") && !value.includes("..");
}
