import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { rootPath } from "./config";
import type { PublicServiceExposure, PublicServiceTarget } from "./platform-infra-public-service";
import { assertNoDuplicateYamlMappingKeys, createYamlFieldReader, readYamlRecord, resolveRepoPath } from "./platform-infra-ops-library";

export const sub2RankConfigPath = rootPath("config", "platform-infra", "sub2rank.yaml");
export const sub2RankConfigLabel = "config/platform-infra/sub2rank.yaml";

const y = createYamlFieldReader(sub2RankConfigLabel);

export interface Sub2RankConfig {
  version: number;
  kind: "platform-infra-sub2rank";
  warnings: string[];
  metadata: { id: string; owner: string; relatedIssues: number[] };
  defaults: { targetId: string };
  webProbe: Sub2RankWebProbeConfig;
  application: {
    repository: string;
    remote: string;
    branch: string;
    sourceRoot: string;
    configRef: string;
    sourceConfigPath: string;
    dockerfile: string;
    runtimeTarget: string;
    configText: string;
    configSha256: string;
    sourceCommit: string;
    sourceClean: boolean;
    sourceRemotePresent: boolean;
    sourceRemoteMatches: boolean;
    sourceConfigPathMatches: boolean;
    configMatchesSourceCommit: boolean;
    deploymentBlockPresent: boolean;
    automaticCreditEnabled: boolean;
    webAuthUsername: string;
    temporalAddressEnv: string;
    temporalNamespace: string;
    temporalTaskQueue: string;
    server: { listenPort: number; workerHealthPort: number; databasePath: string; envKeys: string[] };
  };
  image: { repository: string; pullPolicy: "Always" | "IfNotPresent" | "Never" };
  delivery: {
    enabled: boolean;
    pipeline: {
      namespace: string;
      name: string;
      runPrefix: string;
      serviceAccountName: string;
      timeout: string;
      workspaceSize: string;
      toolsImage: string;
      buildkitImage: string;
      sourceSnapshotPrefix: string;
    };
    build: {
      networkMode: "host";
      proxy: { http: string; https: string; all: string; noProxy: string[] };
    };
    gitops: {
      readUrl: string;
      writeUrl: string;
      branch: string;
      manifestPath: string;
      releaseStatePath: string;
      maxPushAttempts: number;
      author: { name: string; email: string };
      application: {
        name: string;
        namespace: string;
        project: string;
        path: string;
        destinationNamespace: string;
        automated: boolean;
      };
    };
    profiles: Record<string, Sub2RankDeliveryProfile>;
  };
  targets: Sub2RankTarget[];
  runtime: {
    sharedNetworkPolicyRef: { name: string; managedBySub2Rank: false };
    service: { name: string; port: number };
    worker: { name: string; healthPort: number };
    temporal: { addressEnv: string; address: string };
    configMap: { name: string; key: string; mountPath: string };
    command: string[];
    workerCommand: string[];
    storage: { claimName: string; size: string; mountPath: string };
    hostMounts: Array<{ source: string; target: string; readOnly: true }>;
    secret: ResolvedSecretDeclaration & { configRef: string; declarationId: string; requiredTargetKeys: string[]; appEnvTargetKeys: string[] };
    probes: { healthPath: string; initialDelaySeconds: number; periodSeconds: number; timeoutSeconds: number; failureThreshold: number };
    rollout: { fieldManager: string; waitTimeoutSeconds: number };
  };
}

export interface Sub2RankWebProbePage {
  id: string;
  path: string;
  readySelector: string;
  screenshotName: string;
}

export interface Sub2RankWebProbeSmokeProfile {
  navigationTimeoutMs: number;
  commandTimeoutSeconds: number;
  settleMs: number;
  viewport: { width: number; height: number };
  pages: Sub2RankWebProbePage[];
  outputLimits: { failures: number; network: number };
}

export interface Sub2RankWebProbeConfig {
  enabled: boolean;
  runner: { node: string; lane: string };
  origin: { browserProxyMode: "auto" | "direct" | "proxy"; nativeBaseUrl: string };
  authentication: {
    path: string;
    usernameField: string;
    passwordField: string;
    passwordTargetKey: string;
    cookieNames: string[];
  };
  defaultSmokeProfile: string;
  smokeProfiles: Record<string, Sub2RankWebProbeSmokeProfile>;
}

export interface Sub2RankTarget extends PublicServiceTarget {
  enabled: boolean;
  manageNamespace: boolean;
  applicationRuntimeTarget: string;
  temporalTaskQueue: string;
  scoreScheduleWorkflowId: string;
  publicEdgeEnabled: boolean;
  secret: ResolvedSecretDeclaration;
  publicExposure: PublicServiceExposure & {
    frpc: PublicServiceExposure["frpc"] & {
      configMapName: string;
      configKey: string;
      tokenEnvName: string;
      tokenTargetKey: string;
    };
  };
}

export interface Sub2RankDeliveryProfile {
  targetId: string;
  sourceBranch: string;
  pipeline: Sub2RankConfig["delivery"]["pipeline"];
  gitops: Sub2RankConfig["delivery"]["gitops"];
}

interface ParsedTarget {
  id: string;
  route: string;
  namespace: string;
  enabled: boolean;
  replicas: number;
  manageNamespace: boolean;
  applicationRuntimeTarget: string;
  secretDeclarationId: string;
  temporalTaskQueue: string;
  scoreScheduleWorkflowId: string;
  publicEdgeEnabled: boolean;
  exposure: Omit<Sub2RankTarget["publicExposure"], "frpc"> & {
    frpc: Omit<Sub2RankTarget["publicExposure"]["frpc"], "secretName" | "secretKey" | "tokenSourceRef" | "tokenSourceKey">;
  };
}

export interface ResolvedSecretDeclaration {
  declarationId: string;
  targetId: string;
  route: string;
  namespace: string;
  scope: string;
  secretName: string;
  mappings: Array<{ sourceRef: string; sourceKey: string; targetKey: string }>;
}

export function readSub2RankConfig(): Sub2RankConfig {
  const root = readYamlRecord<Record<string, unknown>>(sub2RankConfigPath, "platform-infra-sub2rank");
  const version = y.integerField(root, "version", "");
  if (version !== 1) throw new Error(`${sub2RankConfigLabel}.version must be 1`);
  const metadataRecord = y.objectField(root, "metadata", "");
  const defaultsRecord = y.objectField(root, "defaults", "");
  const webProbeRecord = y.objectField(root, "webProbe", "");
  const applicationRecord = y.objectField(root, "application", "");
  const imageRecord = y.objectField(root, "image", "");
  const deliveryRecord = y.objectField(root, "delivery", "");
  const runtimeRecord = y.objectField(root, "runtime", "");
  const defaults = { targetId: simpleId(y.stringField(defaultsRecord, "targetId", "defaults"), "defaults.targetId") };
  const webProbe = parseWebProbe(webProbeRecord);
  const application = parseApplication(applicationRecord);
  const image = parseImage(imageRecord);
  const delivery = parseDelivery(deliveryRecord);
  const runtimeBase = parseRuntime(runtimeRecord);
  const secret = resolveSecretDeclaration(runtimeBase.secret.configRef, runtimeBase.secret.declarationId);
  const targets = parseTargets(root.targets, runtimeBase.secret.configRef, secret);
  if (!targets.some((target) => target.id === defaults.targetId)) throw new Error(`${sub2RankConfigLabel}.targets must include defaults.targetId ${defaults.targetId}`);
  const warnings = validateResolvedConfig(application, image, delivery, webProbe, runtimeBase, secret, targets);
  return {
    version,
    kind: "platform-infra-sub2rank",
    warnings,
    metadata: {
      id: y.stringField(metadataRecord, "id", "metadata"),
      owner: y.stringField(metadataRecord, "owner", "metadata"),
      relatedIssues: y.numberArrayField(metadataRecord, "relatedIssues", "metadata"),
    },
    defaults,
    webProbe,
    application,
    image,
    delivery,
    targets,
    runtime: { ...runtimeBase, secret: { ...runtimeBase.secret, ...secret } },
  };
}

function parseWebProbe(record: Record<string, unknown>): Sub2RankWebProbeConfig {
  const runner = y.objectField(record, "runner", "webProbe");
  const origin = y.objectField(record, "origin", "webProbe");
  const authentication = y.objectField(record, "authentication", "webProbe");
  const profilesRecord = y.objectField(record, "smokeProfiles", "webProbe");
  const smokeProfiles: Record<string, Sub2RankWebProbeSmokeProfile> = {};
  for (const [profileName, value] of Object.entries(profilesRecord)) {
    const profilePath = `webProbe.smokeProfiles.${profileName}`;
    const profile = y.asRecord(value, profilePath);
    const viewport = y.objectField(profile, "viewport", profilePath);
    const outputLimits = y.objectField(profile, "outputLimits", profilePath);
    const pages = y.arrayOfRecords(profile.pages, `${profilePath}.pages`).map((page, index) => {
      const pagePath = `${profilePath}.pages[${index}]`;
      const screenshotName = y.stringField(page, "screenshotName", pagePath);
      if (!/^[A-Za-z0-9._-]+[.]png$/u.test(screenshotName)) throw new Error(`${sub2RankConfigLabel}.${pagePath}.screenshotName must be a safe png filename`);
      return {
        id: simpleId(y.stringField(page, "id", pagePath), `${pagePath}.id`),
        path: y.apiPathField(page, "path", pagePath),
        readySelector: nonEmptyText(y.stringField(page, "readySelector", pagePath), `${pagePath}.readySelector`),
        screenshotName,
      };
    });
    if (pages.length === 0) throw new Error(`${sub2RankConfigLabel}.${profilePath}.pages must not be empty`);
    smokeProfiles[simpleId(profileName, profilePath)] = {
      navigationTimeoutMs: positiveInteger(profile, "navigationTimeoutMs", profilePath),
      commandTimeoutSeconds: positiveInteger(profile, "commandTimeoutSeconds", profilePath),
      settleMs: nonNegativeInteger(profile.settleMs, `${profilePath}.settleMs`),
      viewport: {
        width: positiveInteger(viewport, "width", `${profilePath}.viewport`),
        height: positiveInteger(viewport, "height", `${profilePath}.viewport`),
      },
      pages,
      outputLimits: {
        failures: positiveInteger(outputLimits, "failures", `${profilePath}.outputLimits`),
        network: positiveInteger(outputLimits, "network", `${profilePath}.outputLimits`),
      },
    };
  }
  const defaultSmokeProfile = simpleId(y.stringField(record, "defaultSmokeProfile", "webProbe"), "webProbe.defaultSmokeProfile");
  if (smokeProfiles[defaultSmokeProfile] === undefined) throw new Error(`${sub2RankConfigLabel}.webProbe.defaultSmokeProfile is not declared`);
  return {
    enabled: y.booleanField(record, "enabled", "webProbe"),
    runner: {
      node: simpleId(y.stringField(runner, "node", "webProbe.runner"), "webProbe.runner.node"),
      lane: simpleId(y.stringField(runner, "lane", "webProbe.runner"), "webProbe.runner.lane"),
    },
    origin: {
      browserProxyMode: y.enumField(origin, "browserProxyMode", "webProbe.origin", ["auto", "direct", "proxy"] as const),
      nativeBaseUrl: httpUrlValue(y.stringField(origin, "nativeBaseUrl", "webProbe.origin"), "webProbe.origin.nativeBaseUrl"),
    },
    authentication: {
      path: y.apiPathField(authentication, "path", "webProbe.authentication"),
      usernameField: simpleId(y.stringField(authentication, "usernameField", "webProbe.authentication"), "webProbe.authentication.usernameField"),
      passwordField: simpleId(y.stringField(authentication, "passwordField", "webProbe.authentication"), "webProbe.authentication.passwordField"),
      passwordTargetKey: y.envKeyField(authentication, "passwordTargetKey", "webProbe.authentication"),
      cookieNames: y.stringArrayField(authentication, "cookieNames", "webProbe.authentication").map((name) => simpleId(name, "webProbe.authentication.cookieNames")),
    },
    defaultSmokeProfile,
    smokeProfiles,
  };
}

export function resolveSub2RankTarget(config: Sub2RankConfig, targetId: string | null): Sub2RankTarget {
  const selected = targetId ?? config.defaults.targetId;
  const target = config.targets.find((item) => item.id === selected);
  if (target === undefined) throw new Error(`unknown Sub2Rank target ${selected}; known targets: ${config.targets.map((item) => item.id).join(", ")}`);
  if (!target.enabled) throw new Error(`Sub2Rank target ${selected} is disabled in ${sub2RankConfigLabel}`);
  return target;
}

function parseApplication(record: Record<string, unknown>): Sub2RankConfig["application"] {
  const repository = repositoryValue(y.stringField(record, "repository", "application"), "application.repository");
  const remote = y.stringField(record, "remote", "application");
  const branch = simpleId(y.stringField(record, "branch", "application"), "application.branch");
  const sourceRoot = y.absolutePathField(record, "sourceRoot", "application");
  const configRef = y.absolutePathField(record, "configRef", "application");
  const sourceConfigPath = safeRelativePath(y.stringField(record, "sourceConfigPath", "application"), "application.sourceConfigPath");
  const dockerfile = safeRelativePath(y.stringField(record, "dockerfile", "application"), "application.dockerfile");
  const runtimeTarget = simpleId(y.stringField(record, "runtimeTarget", "application"), "application.runtimeTarget");
  if (!configRef.startsWith(`${sourceRoot.replace(/\/+$/u, "")}/`)) throw new Error(`${sub2RankConfigLabel}.application.configRef must be inside application.sourceRoot`);
  if (!existsSync(resolve(sourceRoot, dockerfile))) throw new Error(`${sub2RankConfigLabel}.application.dockerfile does not exist under application.sourceRoot`);
  const configText = readFileSync(configRef, "utf8");
  assertNoDuplicateYamlMappingKeys(configText, configRef);
  const app = y.asRecord(Bun.YAML.parse(configText), configRef);
  if (app.kind !== "Sub2Rank") throw new Error(`${configRef}.kind must be Sub2Rank`);
  const appRuntime = app.runtime;
  if (typeof appRuntime !== "object" || appRuntime === null || Array.isArray(appRuntime)) throw new Error(`${configRef}.runtime must be an object`);
  const serverTargets = (appRuntime as Record<string, unknown>).serverTargets;
  if (typeof serverTargets !== "object" || serverTargets === null || Array.isArray(serverTargets)) throw new Error(`${configRef}.runtime.serverTargets must be an object`);
  const serverValue = (serverTargets as Record<string, unknown>)[runtimeTarget];
  if (typeof serverValue !== "object" || serverValue === null || Array.isArray(serverValue)) throw new Error(`${configRef}.runtime.serverTargets.${runtimeTarget} must be an object`);
  const server = serverValue as Record<string, unknown>;
  const appTemporal = app.temporal;
  if (typeof appTemporal !== "object" || appTemporal === null || Array.isArray(appTemporal)) throw new Error(`${configRef}.temporal must be an object`);
  const lottery = app.lottery;
  if (typeof lottery !== "object" || lottery === null || Array.isArray(lottery)) throw new Error(`${configRef}.lottery must be an object`);
  const automaticCredit = (lottery as Record<string, unknown>).automaticCredit;
  if (typeof automaticCredit !== "object" || automaticCredit === null || Array.isArray(automaticCredit)) throw new Error(`${configRef}.lottery.automaticCredit must be an object`);
  const automaticCreditEnabled = (automaticCredit as Record<string, unknown>).enabled;
  if (typeof automaticCreditEnabled !== "boolean") throw new Error(`${configRef}.lottery.automaticCredit.enabled must be a boolean`);
  const webAuth = app.webAuth;
  if (typeof webAuth !== "object" || webAuth === null || Array.isArray(webAuth)) throw new Error(`${configRef}.webAuth must be an object`);
  const webAuthUsername = simpleId(stringValue((webAuth as Record<string, unknown>).username, `${configRef}.webAuth.username`), `${configRef}.webAuth.username`);
  const sourceCommit = gitText(sourceRoot, ["rev-parse", "HEAD"], "resolve Sub2Rank source commit");
  if (!/^[a-f0-9]{40}$/u.test(sourceCommit)) throw new Error(`${sourceRoot} HEAD must resolve to a full Git commit`);
  const statusShort = gitText(sourceRoot, ["status", "--porcelain"], "read Sub2Rank source status", true);
  const observedRemote = gitText(sourceRoot, ["remote", "get-url", "origin"], "read Sub2Rank origin", true);
  const configRelativePath = relative(sourceRoot, configRef).replaceAll("\\", "/");
  const committedConfig = gitText(sourceRoot, ["show", `${sourceCommit}:${configRelativePath}`], "read committed Sub2Rank application config", true, false);
  return {
    repository,
    remote,
    branch,
    sourceRoot,
    configRef,
    sourceConfigPath,
    dockerfile,
    runtimeTarget,
    configText,
    configSha256: createHash("sha256").update(configText).digest("hex"),
    sourceCommit,
    sourceClean: statusShort.length === 0,
    sourceRemotePresent: observedRemote.length > 0,
    sourceRemoteMatches: sameRepositoryIdentity(remote, observedRemote),
    sourceConfigPathMatches: configRelativePath === sourceConfigPath,
    configMatchesSourceCommit: committedConfig === configText.trimEnd(),
    deploymentBlockPresent: app.deployment !== undefined,
    automaticCreditEnabled,
    webAuthUsername,
    temporalAddressEnv: envKeyValue((appTemporal as Record<string, unknown>).addressEnv, `${configRef}.temporal.addressEnv`),
    temporalNamespace: stringValue((appTemporal as Record<string, unknown>).namespace, `${configRef}.temporal.namespace`),
    temporalTaskQueue: stringValue((appTemporal as Record<string, unknown>).taskQueue, `${configRef}.temporal.taskQueue`),
    server: {
      listenPort: integerValue(server.listenPort, `${configRef}.runtime.serverTargets.${runtimeTarget}.listenPort`),
      workerHealthPort: integerValue(server.workerHealthPort, `${configRef}.runtime.serverTargets.${runtimeTarget}.workerHealthPort`),
      databasePath: absolutePathValue(server.databasePath, `${configRef}.runtime.serverTargets.${runtimeTarget}.databasePath`),
      envKeys: [...new Set([
        "adminTokenEnv",
        "sub2apiAdminEmailEnv",
        "sub2apiAdminPasswordEnv",
        "webPasswordEnv",
        "apiKeyEnv",
        "sessionSecretEnv",
      ].map((key) => envKeyValue(server[key], `${configRef}.runtime.serverTargets.${runtimeTarget}.${key}`)))],
    },
  };
}

function parseImage(record: Record<string, unknown>): Sub2RankConfig["image"] {
  const repository = y.stringField(record, "repository", "image");
  const pullPolicy = y.enumField(record, "pullPolicy", "image", ["Always", "IfNotPresent", "Never"] as const);
  if (!/^[A-Za-z0-9._:/-]+$/u.test(repository) || repository.endsWith("/")) throw new Error(`${sub2RankConfigLabel}.image.repository has an unsupported format`);
  return { repository, pullPolicy };
}

function parseDelivery(record: Record<string, unknown>): Sub2RankConfig["delivery"] {
  const build = y.objectField(record, "build", "delivery");
  const proxy = y.objectField(build, "proxy", "delivery.build");
  const pipeline = parseDeliveryPipeline(y.objectField(record, "pipeline", "delivery"), "delivery.pipeline");
  const gitops = parseDeliveryGitOps(y.objectField(record, "gitops", "delivery"), "delivery.gitops");
  const profilesRecord = record.profiles === undefined ? {} : y.objectField(record, "profiles", "delivery");
  const profiles: Record<string, Sub2RankDeliveryProfile> = {};
  for (const [profileId, value] of Object.entries(profilesRecord)) {
    const path = `delivery.profiles.${profileId}`;
    const profile = y.asRecord(value, path);
    profiles[simpleId(profileId, path)] = {
      targetId: simpleId(y.stringField(profile, "targetId", path), `${path}.targetId`),
      sourceBranch: simpleId(y.stringField(profile, "sourceBranch", path), `${path}.sourceBranch`),
      pipeline: parseDeliveryPipeline(y.objectField(profile, "pipeline", path), `${path}.pipeline`),
      gitops: parseDeliveryGitOps(y.objectField(profile, "gitops", path), `${path}.gitops`),
    };
  }
  return {
    enabled: y.booleanField(record, "enabled", "delivery"),
    pipeline,
    build: {
      networkMode: y.enumField(build, "networkMode", "delivery.build", ["host"] as const),
      proxy: {
        http: httpUrlValue(y.stringField(proxy, "http", "delivery.build.proxy"), "delivery.build.proxy.http"),
        https: httpUrlValue(y.stringField(proxy, "https", "delivery.build.proxy"), "delivery.build.proxy.https"),
        all: httpUrlValue(y.stringField(proxy, "all", "delivery.build.proxy"), "delivery.build.proxy.all"),
        noProxy: y.stringArrayField(proxy, "noProxy", "delivery.build.proxy"),
      },
    },
    gitops,
    profiles,
  };
}

function parseDeliveryPipeline(record: Record<string, unknown>, path: string): Sub2RankConfig["delivery"]["pipeline"] {
  const timeout = y.stringField(record, "timeout", path);
  if (!/^[1-9][0-9]*s$/u.test(timeout)) throw new Error(`${sub2RankConfigLabel}.${path}.timeout must be positive seconds`);
  const sourceSnapshotPrefix = y.stringField(record, "sourceSnapshotPrefix", path);
  if (!/^refs\/[A-Za-z0-9._/-]+$/u.test(sourceSnapshotPrefix) || sourceSnapshotPrefix.includes("..")) throw new Error(`${sub2RankConfigLabel}.${path}.sourceSnapshotPrefix must be a safe refs/ prefix`);
  return {
    namespace: y.kubernetesNameField(record, "namespace", path),
    name: y.kubernetesNameField(record, "name", path),
    runPrefix: y.kubernetesNameField(record, "runPrefix", path),
    serviceAccountName: y.kubernetesNameField(record, "serviceAccountName", path),
    timeout,
    workspaceSize: storageSize(y.stringField(record, "workspaceSize", path)),
    toolsImage: imageValue(y.stringField(record, "toolsImage", path), `${path}.toolsImage`),
    buildkitImage: imageValue(y.stringField(record, "buildkitImage", path), `${path}.buildkitImage`),
    sourceSnapshotPrefix,
  };
}

function parseDeliveryGitOps(record: Record<string, unknown>, path: string): Sub2RankConfig["delivery"]["gitops"] {
  const author = y.objectField(record, "author", path);
  const application = y.objectField(record, "application", path);
  const email = y.stringField(author, "email", `${path}.author`);
  if (!/^[^@\s]+@[^@\s]+$/u.test(email)) throw new Error(`${sub2RankConfigLabel}.${path}.author.email must be an email address`);
  return {
    readUrl: httpUrlValue(y.stringField(record, "readUrl", path), `${path}.readUrl`),
    writeUrl: httpUrlValue(y.stringField(record, "writeUrl", path), `${path}.writeUrl`),
    branch: simpleId(y.stringField(record, "branch", path), `${path}.branch`),
    manifestPath: safeRelativePath(y.stringField(record, "manifestPath", path), `${path}.manifestPath`),
    releaseStatePath: safeRelativePath(y.stringField(record, "releaseStatePath", path), `${path}.releaseStatePath`),
    maxPushAttempts: positiveInteger(record, "maxPushAttempts", path),
    author: { name: y.stringField(author, "name", `${path}.author`), email },
    application: {
      name: y.kubernetesNameField(application, "name", `${path}.application`),
      namespace: y.kubernetesNameField(application, "namespace", `${path}.application`),
      project: y.kubernetesNameField(application, "project", `${path}.application`),
      path: safeRelativePath(y.stringField(application, "path", `${path}.application`), `${path}.application.path`),
      destinationNamespace: y.kubernetesNameField(application, "destinationNamespace", `${path}.application`),
      automated: y.booleanField(application, "automated", `${path}.application`),
    },
  };
}

function parseRuntime(record: Record<string, unknown>): Omit<Sub2RankConfig["runtime"], "secret"> & {
  secret: { configRef: string; declarationId: string; requiredTargetKeys: string[]; appEnvTargetKeys: string[] };
} {
  const service = y.objectField(record, "service", "runtime");
  const worker = y.objectField(record, "worker", "runtime");
  const temporal = y.objectField(record, "temporal", "runtime");
  const sharedNetworkPolicyRef = y.objectField(record, "sharedNetworkPolicyRef", "runtime");
  const configMap = y.objectField(record, "configMap", "runtime");
  const storage = y.objectField(record, "storage", "runtime");
  const hostMounts = y.arrayOfRecords(record.hostMounts, "runtime.hostMounts").map((mount, index) => ({
    source: y.absolutePathField(mount, "source", `runtime.hostMounts[${index}]`),
    target: y.absolutePathField(mount, "target", `runtime.hostMounts[${index}]`),
    readOnly: requiredTrueField(mount, "readOnly", `runtime.hostMounts[${index}]`),
  }));
  const secret = y.objectField(record, "secret", "runtime");
  const probes = y.objectField(record, "probes", "runtime");
  const rollout = y.objectField(record, "rollout", "runtime");
  const configRefValue = y.stringField(secret, "configRef", "runtime.secret");
  return {
    sharedNetworkPolicyRef: {
      name: y.kubernetesNameField(sharedNetworkPolicyRef, "name", "runtime.sharedNetworkPolicyRef"),
      managedBySub2Rank: disabledField(sharedNetworkPolicyRef, "managedBySub2Rank", "runtime.sharedNetworkPolicyRef"),
    },
    service: { name: y.kubernetesNameField(service, "name", "runtime.service"), port: y.portField(service, "port", "runtime.service") },
    worker: { name: y.kubernetesNameField(worker, "name", "runtime.worker"), healthPort: y.portField(worker, "healthPort", "runtime.worker") },
    temporal: {
      addressEnv: y.envKeyField(temporal, "addressEnv", "runtime.temporal"),
      address: y.stringField(temporal, "address", "runtime.temporal"),
    },
    configMap: {
      name: y.kubernetesNameField(configMap, "name", "runtime.configMap"),
      key: y.stringField(configMap, "key", "runtime.configMap"),
      mountPath: y.absolutePathField(configMap, "mountPath", "runtime.configMap"),
    },
    command: y.stringArrayField(record, "command", "runtime"),
    workerCommand: y.stringArrayField(record, "workerCommand", "runtime"),
    storage: {
      claimName: y.kubernetesNameField(storage, "claimName", "runtime.storage"),
      size: storageSize(y.stringField(storage, "size", "runtime.storage")),
      mountPath: y.absolutePathField(storage, "mountPath", "runtime.storage"),
    },
    hostMounts,
    secret: {
      configRef: configRefValue,
      declarationId: simpleId(y.stringField(secret, "declarationId", "runtime.secret"), "runtime.secret.declarationId"),
      requiredTargetKeys: y.stringArrayField(secret, "requiredTargetKeys", "runtime.secret").map((key) => envKeyValue(key, "runtime.secret.requiredTargetKeys")),
      appEnvTargetKeys: y.stringArrayField(secret, "appEnvTargetKeys", "runtime.secret").map((key) => envKeyValue(key, "runtime.secret.appEnvTargetKeys")),
    },
    probes: {
      healthPath: y.apiPathField(probes, "healthPath", "runtime.probes"),
      initialDelaySeconds: positiveInteger(probes, "initialDelaySeconds", "runtime.probes"),
      periodSeconds: positiveInteger(probes, "periodSeconds", "runtime.probes"),
      timeoutSeconds: positiveInteger(probes, "timeoutSeconds", "runtime.probes"),
      failureThreshold: positiveInteger(probes, "failureThreshold", "runtime.probes"),
    },
    rollout: {
      fieldManager: simpleId(y.stringField(rollout, "fieldManager", "runtime.rollout"), "runtime.rollout.fieldManager"),
      waitTimeoutSeconds: positiveInteger(rollout, "waitTimeoutSeconds", "runtime.rollout"),
    },
  };
}

function parseTargets(value: unknown, secretConfigRef: string, defaultSecret: ResolvedSecretDeclaration): Sub2RankTarget[] {
  const records = y.arrayOfRecords(value, "targets");
  const ids = new Set<string>();
  return records.map((record, index) => {
    const path = `targets[${index}]`;
    const parsed = parseTarget(record, path);
    if (ids.has(parsed.id)) throw new Error(`${sub2RankConfigLabel}.targets contains duplicate id ${parsed.id}`);
    ids.add(parsed.id);
    const secret = parsed.secretDeclarationId === defaultSecret.declarationId
      ? defaultSecret
      : resolveSecretDeclaration(secretConfigRef, parsed.secretDeclarationId);
    if (secret.route !== parsed.route || secret.namespace !== parsed.namespace) {
      throw new Error(`${sub2RankConfigLabel}.${path}.secretDeclarationId must resolve to route ${parsed.route} and namespace ${parsed.namespace}`);
    }
    const mapping = secret.mappings.find((item) => item.targetKey === parsed.exposure.frpc.tokenTargetKey);
    if (mapping === undefined) throw new Error(`${sub2RankConfigLabel}.${path}.publicExposure.frpc.tokenTargetKey is not provided by runtime.secret.declarationId`);
    return {
      id: parsed.id,
      route: parsed.route,
      namespace: parsed.namespace,
      enabled: parsed.enabled,
      replicas: parsed.replicas,
      manageNamespace: parsed.manageNamespace,
      applicationRuntimeTarget: parsed.applicationRuntimeTarget,
      temporalTaskQueue: parsed.temporalTaskQueue,
      scoreScheduleWorkflowId: parsed.scoreScheduleWorkflowId,
      publicEdgeEnabled: parsed.publicEdgeEnabled,
      secret,
      publicExposure: {
        ...parsed.exposure,
        frpc: {
          ...parsed.exposure.frpc,
          secretName: secret.secretName,
          secretKey: mapping.targetKey,
          tokenSourceRef: mapping.sourceRef,
          tokenSourceKey: mapping.sourceKey,
        },
      },
    };
  });
}

function parseTarget(record: Record<string, unknown>, path: string): ParsedTarget {
  const exposure = y.objectField(record, "publicExposure", path);
  const dns = y.objectField(exposure, "dns", `${path}.publicExposure`);
  const frpc = y.objectField(exposure, "frpc", `${path}.publicExposure`);
  const pk01 = y.objectField(exposure, "pk01", `${path}.publicExposure`);
  const publicEdge = y.objectField(record, "publicEdge", path);
  return {
    id: simpleId(y.stringField(record, "id", path), `${path}.id`),
    route: routeValue(y.stringField(record, "route", path), `${path}.route`),
    namespace: y.kubernetesNameField(record, "namespace", path),
    enabled: y.booleanField(record, "enabled", path),
    replicas: positiveInteger(record, "replicas", path),
    manageNamespace: y.booleanField(record, "manageNamespace", path),
    applicationRuntimeTarget: simpleId(y.stringField(record, "applicationRuntimeTarget", path), `${path}.applicationRuntimeTarget`),
    secretDeclarationId: simpleId(y.stringField(record, "secretDeclarationId", path), `${path}.secretDeclarationId`),
    temporalTaskQueue: simpleId(y.stringField(record, "temporalTaskQueue", path), `${path}.temporalTaskQueue`),
    scoreScheduleWorkflowId: simpleId(y.stringField(record, "scoreScheduleWorkflowId", path), `${path}.scoreScheduleWorkflowId`),
    publicEdgeEnabled: y.booleanField(publicEdge, "enabled", `${path}.publicEdge`),
    exposure: {
      enabled: y.booleanField(exposure, "enabled", `${path}.publicExposure`),
      publicBaseUrl: y.httpsUrlField(exposure, "publicBaseUrl", `${path}.publicExposure`),
      dns: {
        hostname: y.hostField(dns, "hostname", `${path}.publicExposure.dns`),
        expectedA: y.hostField(dns, "expectedA", `${path}.publicExposure.dns`),
        resolvers: y.stringArrayField(dns, "resolvers", `${path}.publicExposure.dns`),
      },
      frpc: {
        deploymentName: y.kubernetesNameField(frpc, "deploymentName", `${path}.publicExposure.frpc`),
        configMapName: y.kubernetesNameField(frpc, "configMapName", `${path}.publicExposure.frpc`),
        configKey: y.stringField(frpc, "configKey", `${path}.publicExposure.frpc`),
        image: imageValue(y.stringField(frpc, "image", `${path}.publicExposure.frpc`), `${path}.publicExposure.frpc.image`),
        serverAddr: y.hostField(frpc, "serverAddr", `${path}.publicExposure.frpc`),
        serverPort: y.portField(frpc, "serverPort", `${path}.publicExposure.frpc`),
        proxyName: simpleId(y.stringField(frpc, "proxyName", `${path}.publicExposure.frpc`), `${path}.publicExposure.frpc.proxyName`),
        remotePort: y.portField(frpc, "remotePort", `${path}.publicExposure.frpc`),
        localIP: y.hostField(frpc, "localIP", `${path}.publicExposure.frpc`),
        localPort: y.portField(frpc, "localPort", `${path}.publicExposure.frpc`),
        tokenEnvName: y.envKeyField(frpc, "tokenEnvName", `${path}.publicExposure.frpc`),
        tokenTargetKey: y.envKeyField(frpc, "tokenTargetKey", `${path}.publicExposure.frpc`),
      },
      pk01: {
        route: routeValue(y.stringField(pk01, "route", `${path}.publicExposure.pk01`), `${path}.publicExposure.pk01.route`),
        caddyConfigPath: y.absolutePathField(pk01, "caddyConfigPath", `${path}.publicExposure.pk01`),
        caddyServiceName: simpleId(y.stringField(pk01, "caddyServiceName", `${path}.publicExposure.pk01`), `${path}.publicExposure.pk01.caddyServiceName`),
        responseHeaderTimeoutSeconds: positiveInteger(pk01, "responseHeaderTimeoutSeconds", `${path}.publicExposure.pk01`),
      },
    },
  };
}

function resolveSecretDeclaration(configRef: string, declarationId: string): ResolvedSecretDeclaration {
  const path = resolveRepoPath(configRef);
  const root = readYamlRecord<Record<string, unknown>>(path, "unidesk-secret-distribution");
  const declarations = Array.isArray(root.kubernetesSecrets) ? root.kubernetesSecrets : [];
  const declaration = declarations.find((item) => typeof item === "object" && item !== null && !Array.isArray(item) && (item as Record<string, unknown>).name === declarationId);
  if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) throw new Error(`${configRef} does not contain kubernetesSecrets name=${declarationId}`);
  const item = declaration as Record<string, unknown>;
  const targetId = stringValue(item.targetId, `${configRef}.kubernetesSecrets[name=${declarationId}].targetId`);
  const secretName = kubernetesNameValue(item.secretName, `${configRef}.kubernetesSecrets[name=${declarationId}].secretName`);
  const targets = Array.isArray(root.targets) ? root.targets : [];
  const target = targets.find((value) => typeof value === "object" && value !== null && !Array.isArray(value) && (value as Record<string, unknown>).id === targetId);
  if (typeof target !== "object" || target === null || Array.isArray(target)) throw new Error(`${configRef} does not contain targets id=${targetId}`);
  const targetRecord = target as Record<string, unknown>;
  const data = Array.isArray(item.data) ? item.data : [];
  const mappings = data.map((value, index) => {
    if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configRef}.kubernetesSecrets[name=${declarationId}].data[${index}] must be an object`);
    const mapping = value as Record<string, unknown>;
    return {
      sourceRef: sourceRefValue(mapping.sourceRef, `${configRef}.kubernetesSecrets[name=${declarationId}].data[${index}].sourceRef`),
      sourceKey: secretSourceKeyValue(mapping.sourceKey, `${configRef}.kubernetesSecrets[name=${declarationId}].data[${index}].sourceKey`),
      targetKey: envKeyValue(mapping.targetKey, `${configRef}.kubernetesSecrets[name=${declarationId}].data[${index}].targetKey`),
    };
  });
  return {
    declarationId,
    targetId,
    route: routeValue(stringValue(targetRecord.route, `${configRef}.targets[id=${targetId}].route`), `${configRef}.targets[id=${targetId}].route`),
    namespace: kubernetesNameValue(targetRecord.namespace, `${configRef}.targets[id=${targetId}].namespace`),
    scope: simpleId(stringValue(targetRecord.scope, `${configRef}.targets[id=${targetId}].scope`), `${configRef}.targets[id=${targetId}].scope`),
    secretName,
    mappings,
  };
}

function validateResolvedConfig(
  application: Sub2RankConfig["application"],
  image: Sub2RankConfig["image"],
  delivery: Sub2RankConfig["delivery"],
  webProbe: Sub2RankConfig["webProbe"],
  runtime: ReturnType<typeof parseRuntime>,
  secret: ResolvedSecretDeclaration,
  targets: Sub2RankTarget[],
): string[] {
  const warnings: string[] = [];
  if (!application.sourceConfigPathMatches) warnings.push("application.sourceConfigPath 与 application.configRef 的相对路径不一致");
  if (!application.configMatchesSourceCommit) warnings.push(`application.configRef 与源码提交 ${application.sourceCommit} 中的文件不一致`);
  if (!application.sourceRemoteMatches) warnings.push("application.remote 与 application.sourceRoot 的 origin 不一致");
  if (!sameRepositoryIdentity(application.remote, `https://github.com/${application.repository}.git`)) warnings.push("application.repository 与 application.remote 指向的仓库不一致");
  if (!delivery.enabled) warnings.push("delivery.enabled=false，自动交付当前关闭");
  if (!webProbe.enabled) warnings.push("webProbe.enabled=false，受控浏览器验收当前关闭");
  if (!secret.mappings.some((item) => item.targetKey === webProbe.authentication.passwordTargetKey)) {
    throw new Error(`${sub2RankConfigLabel}.webProbe.authentication.passwordTargetKey is not provided by runtime.secret.declarationId`);
  }
  if (delivery.gitops.maxPushAttempts > 5) warnings.push("delivery.gitops.maxPushAttempts 大于 5，失败时可能延长流水线耗时");
  if (dirname(delivery.gitops.manifestPath).replaceAll("\\", "/") !== delivery.gitops.application.path) warnings.push("delivery.gitops.manifestPath 不在 application.path 的直接子路径下");
  if (!delivery.build.proxy.noProxy.includes("hyueapi.com") || !delivery.build.proxy.noProxy.includes(".hyueapi.com")) throw new Error(`${sub2RankConfigLabel}.delivery.build.proxy.noProxy must retain hyueapi.com and .hyueapi.com`);
  if (!image.repository.startsWith("127.0.0.1:5000/")) warnings.push("image.repository 未使用 NC01 本地 registry");
  const required = new Set(runtime.secret.requiredTargetKeys);
  if (!sameStringSet(application.server.envKeys, runtime.secret.appEnvTargetKeys)) warnings.push("runtime.secret.appEnvTargetKeys 与应用声明的环境变量集合不一致");
  if (application.server.listenPort !== runtime.service.port) warnings.push("runtime.service.port 与应用 listenPort 不一致");
  if (application.server.workerHealthPort !== runtime.worker.healthPort) warnings.push("runtime.worker.healthPort 与应用 workerHealthPort 不一致");
  if (application.temporalAddressEnv !== runtime.temporal.addressEnv) warnings.push("runtime.temporal.addressEnv 与应用 temporal.addressEnv 不一致");
  if (!application.server.databasePath.startsWith(`${runtime.storage.mountPath.replace(/\/+$/u, "")}/`)) warnings.push("应用 databasePath 不在 runtime.storage.mountPath 下");
  if (!delivery.gitops.application.automated) warnings.push("delivery.gitops.application.automated=false，Argo 自动同步当前关闭");
  if (application.deploymentBlockPresent) warnings.push("应用配置仍包含 deployment 段；部署事实应以 platform-infra YAML 为准");
  for (const target of targets) {
    const provided = new Set(target.secret.mappings.map((item) => item.targetKey));
    const missing = [...required].filter((key) => !provided.has(key));
    if (missing.length > 0) throw new Error(`${sub2RankConfigLabel}.runtime.secret.requiredTargetKeys missing from ${runtime.secret.configRef} declaration ${target.secret.declarationId}: ${missing.join(", ")}`);
    if (target.publicExposure.frpc.localPort !== runtime.service.port) warnings.push(`targets[${target.id}].publicExposure.frpc.localPort 与 runtime.service.port 不一致`);
  }
  if (!targets.some((target) => target.namespace === delivery.gitops.application.destinationNamespace)) {
    throw new Error(`${sub2RankConfigLabel}.delivery.gitops.application.destinationNamespace must match one target namespace`);
  }
  for (const [profileId, profile] of Object.entries(delivery.profiles)) {
    const target = targets.find((candidate) => candidate.id === profile.targetId);
    if (!target) throw new Error(`${sub2RankConfigLabel}.delivery.profiles.${profileId}.targetId references missing target ${profile.targetId}`);
    if (target.namespace !== profile.gitops.application.destinationNamespace) throw new Error(`${sub2RankConfigLabel}.delivery.profiles.${profileId}.gitops.application.destinationNamespace must equal target namespace ${target.namespace}`);
    if (profile.gitops.maxPushAttempts > 5) warnings.push(`delivery.profiles.${profileId}.gitops.maxPushAttempts 大于 5，失败时可能延长流水线耗时`);
  }
  return warnings;
}

function positiveInteger(record: Record<string, unknown>, key: string, path: string): number {
  const value = y.integerField(record, key, path);
  if (value <= 0) throw new Error(`${sub2RankConfigLabel}.${path}.${key} must be > 0`);
  return value;
}

function nonNegativeInteger(value: unknown, path: string): number {
  if (!Number.isInteger(value) || Number(value) < 0) throw new Error(`${sub2RankConfigLabel}.${path} must be >= 0`);
  return Number(value);
}

function nonEmptyText(value: string, path: string): string {
  const normalized = value.trim();
  if (normalized.length === 0 || normalized.length > 240) throw new Error(`${sub2RankConfigLabel}.${path} must be 1..240 characters`);
  return normalized;
}

function disabledField(record: Record<string, unknown>, key: string, path: string): false {
  const value = y.booleanField(record, key, path);
  if (value) throw new Error(`${sub2RankConfigLabel}.${path}.${key} must remain false because the resource is shared and owned outside Sub2Rank`);
  return false;
}

function requiredTrueField(record: Record<string, unknown>, key: string, path: string): true {
  const value = y.booleanField(record, key, path);
  if (!value) throw new Error(`${sub2RankConfigLabel}.${path}.${key} must be true`);
  return true;
}

function storageSize(value: string): string {
  if (!/^[1-9][0-9]*(?:Mi|Gi|Ti)$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.runtime.storage.size must use Mi, Gi, or Ti`);
  return value;
}

function simpleId(value: string, path: string): string {
  if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.${path} must be a simple id`);
  return value;
}

function routeValue(value: string, path: string): string {
  if (!/^[A-Za-z0-9:_./-]+$/u.test(value)) throw new Error(`${path} has an unsupported route format`);
  return value;
}

function imageValue(value: string, path: string): string {
  if (!/^[A-Za-z0-9._:/-]+:[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.${path} must be an image reference with tag`);
  return value;
}

function integerValue(value: unknown, path: string): number {
  if (!Number.isInteger(value)) throw new Error(`${path} must be an integer`);
  return Number(value);
}

function absolutePathValue(value: unknown, path: string): string {
  const text = stringValue(value, path);
  if (!text.startsWith("/") || text.includes("..")) throw new Error(`${path} must be an absolute path without ..`);
  return text;
}

function envKeyValue(value: unknown, path: string): string {
  const text = stringValue(value, path);
  if (!/^[A-Z_][A-Z0-9_]*$/u.test(text)) throw new Error(`${path} must be an environment key`);
  return text;
}

function secretSourceKeyValue(value: unknown, path: string): string {
  const text = stringValue(value, path);
  if (text !== "contents" && !/^[A-Z_][A-Z0-9_]*$/u.test(text)) throw new Error(`${path} must be an environment key or contents`);
  return text;
}

function sourceRefValue(value: unknown, path: string): string {
  const text = stringValue(value, path);
  const segments = text.replace(/^~\//u, "").split("/");
  if (segments.includes("..") || !/^(?:~\/|\/)?[A-Za-z0-9._/-]+$/u.test(text)) {
    throw new Error(`${path} must be a sourceRef without path traversal`);
  }
  return text;
}

function safeRelativePath(value: string, path: string): string {
  if (value.startsWith("/") || value.split(/[\\/]/u).includes("..") || !/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.${path} must be a safe relative path`);
  return value;
}

function repositoryValue(value: string, path: string): string {
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.${path} must be owner/repository`);
  return value;
}

function httpUrlValue(value: string, path: string): string {
  let parsed: URL;
  try {
    parsed = new URL(value);
  } catch {
    throw new Error(`${sub2RankConfigLabel}.${path} must be an HTTP URL`);
  }
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${sub2RankConfigLabel}.${path} must be an HTTP URL`);
  if (parsed.username.length > 0 || parsed.password.length > 0 || parsed.hash.length > 0) throw new Error(`${sub2RankConfigLabel}.${path} must not contain credentials or a fragment`);
  return value;
}

function sameRepositoryIdentity(left: string, right: string): boolean {
  const identity = (value: string): string | null => {
    const scp = /^[^/@\s]+@([^:\s]+):(.+)$/u.exec(value.trim());
    let host = scp?.[1] ?? "";
    let path = scp?.[2] ?? "";
    if (host.length === 0 || path.length === 0) {
      try {
        const parsed = new URL(value.trim());
        host = parsed.hostname;
        path = parsed.pathname;
      } catch {
        return null;
      }
    }
    const parts = path.replace(/^\/+|\/+$/gu, "").split("/");
    if (parts.length !== 2) return null;
    return `${host.toLowerCase()}/${parts[0]?.toLowerCase()}/${parts[1]?.replace(/\.git$/iu, "").toLowerCase()}`;
  };
  const leftIdentity = identity(left);
  return leftIdentity !== null && leftIdentity === identity(right);
}

function kubernetesNameValue(value: unknown, path: string): string {
  const text = stringValue(value, path);
  if (!/^[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/u.test(text)) throw new Error(`${path} must be a Kubernetes name`);
  return text;
}

function stringValue(value: unknown, path: string): string {
  if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string`);
  return value;
}

function sameStringSet(left: string[], right: string[]): boolean {
  return left.length === right.length && [...left].sort().every((value, index) => value === [...right].sort()[index]);
}

function gitText(
  cwd: string,
  args: string[],
  label: string,
  allowEmpty = false,
  trim = true,
): string {
  const result = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
  if (result.exitCode !== 0) {
    const stderr = new TextDecoder().decode(result.stderr).replace(/\s+/gu, " ").trim().slice(0, 500);
    throw new Error(`${label} failed (exit ${result.exitCode}): ${stderr}`);
  }
  const output = new TextDecoder().decode(result.stdout);
  const value = trim ? output.trim() : output.trimEnd();
  if (!allowEmpty && value.length === 0) throw new Error(`${label} returned no output`);
  return value;
}
