// SPEC: PJ2026-01060508 Web哨兵 Release A.
// Responsibility: YAML-first PG-capable central Monitor manifests while SQLite remains authoritative.
import { readWebProbeSentinelConfigRef } from "./hwlab-node-web-sentinel-config-ref";
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
import type { MonitorLegacyArtifactGap, MonitorSqliteSource } from "./monitor-sqlite-migration";

export function releaseAMonitorManifests(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource, options: { readonly migrationJobEnabled?: boolean } = {}): readonly Record<string, unknown>[] {
  const config = releaseAMonitorConfig(spec, resolvedSource);
  const { resolved, runtime, namespace, deploymentName, port, secret, pool, labels } = config;
  requireImageRef(imageRef);
  const env = [{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: textAt(secret, "name", resolved.ref), key: textAt(secret, "key", resolved.ref) } } }, { name: "MONITOR_CENTRAL_HOST", value: textAt(runtime, "bindHost", resolved.ref) }, { name: "MONITOR_CENTRAL_PORT", value: String(port) }, { name: "MONITOR_CENTRAL_PG_POOL_MAX", value: String(integerAt(pool, "max", resolved.ref)) }, { name: "MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS", value: String(integerAt(pool, "idleTimeoutSeconds", resolved.ref)) }, { name: "MONITOR_CENTRAL_PAGE_SIZE", value: String(integerAt(runtime, "pageSize", resolved.ref)) }];
  const monitorLabels = { ...labels, "app.kubernetes.io/component": "monitor-service" };
  const manifests = [
    { apiVersion: "v1", kind: "Service", metadata: { name: textAt(runtime, "serviceName", resolved.ref), namespace, labels: monitorLabels }, spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/component": "monitor-service" }, ports: [{ name: "http", port, targetPort: "http" }] } },
    { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: deploymentName, namespace, labels: monitorLabels }, spec: { replicas: integerAt(runtime, "replicas", resolved.ref), selector: { matchLabels: { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/component": "monitor-service" } }, template: { metadata: { labels: monitorLabels }, spec: { containers: [{ name: "monitor", image: imageRef, command: ["bun"], args: ["scripts/monitor-central-service.ts"], env, ports: [{ name: "http", containerPort: port }], readinessProbe: { httpGet: { path: textAt(runtime, "healthPath", resolved.ref), port: "http" } } }] } } } },
  ];
  const migrationJob = releaseAMonitorMigrationJob(spec, imageRef, resolvedSource, { enabled: options.migrationJobEnabled });
  return migrationJob === null ? manifests : [...manifests, migrationJob];
}

export function releaseAMonitorMigrationJob(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource, options: { readonly enabled?: boolean; readonly nameSuffix?: string } = {}): Record<string, unknown> | null {
  const config = releaseAMonitorConfig(spec, resolvedSource);
  const { resolved, runtime, job, namespace, secret, pool, labels } = config;
  requireImageRef(imageRef);
  if ((options.enabled ?? job.enabled) !== true) return null;
  const migrationLabels = { ...labels, "app.kubernetes.io/component": "migration" };
  const sourceVolumes = resolvedSource.artifactSources.map((source, index) => ({ name: source.ownerPvc === resolvedSource.ownerPvc ? "legacy-sqlite" : `artifact-${index}`, source }));
  const mounts = [...sourceVolumes.map((item) => ({ name: item.name, mountPath: item.source.mountPath, readOnly: true })), { name: "snapshot", mountPath: "/migration" }];
  const env = [{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: textAt(secret, "name", resolved.ref), key: textAt(secret, "key", resolved.ref) } } }, { name: "MONITOR_CENTRAL_PG_POOL_MAX", value: String(integerAt(pool, "max", resolved.ref)) }, { name: "MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS", value: String(integerAt(pool, "idleTimeoutSeconds", resolved.ref)) }];
  const cli = ["scripts/cli.ts", "web-probe", "sentinel", "migration"];
  const sourceArgs = ["--node", spec.nodeId, "--lane", spec.lane, "--sentinel", resolvedSource.sentinelId, "--snapshot", "/migration/snapshot.json"];
  return { apiVersion: "batch/v1", kind: "Job", metadata: { name: versionedJobName(textAt(job, "name", resolved.ref), imageRef, options.nameSuffix), namespace, labels: migrationLabels }, spec: { backoffLimit: 0, template: { metadata: { labels: migrationLabels }, spec: { restartPolicy: "Never", initContainers: [{ name: "export", image: imageRef, command: ["bun"], args: [...cli, "export", ...sourceArgs, "--json"], volumeMounts: mounts }, { name: "import", image: imageRef, command: ["bun"], args: [...cli, "import", ...sourceArgs, "--confirm", "--json"], env, volumeMounts: mounts }], containers: [{ name: "verify", image: imageRef, command: ["bun"], args: [...cli, "verify", ...sourceArgs, "--json"], env, volumeMounts: mounts }], volumes: [...sourceVolumes.map((item) => item.source.hostPath === undefined ? { name: item.name, persistentVolumeClaim: { claimName: item.source.ownerPvc, readOnly: true } } : { name: item.name, hostPath: { path: item.source.hostPath, type: "Directory" } }), { name: "snapshot", emptyDir: {} }] } } } };
}

export function releaseAMonitorMigrationPool(spec: HwlabRuntimeLaneSpec, resolvedSource: MonitorSqliteSource): { readonly poolMax: number; readonly idleTimeoutSeconds: number } {
  const { resolved, pool } = releaseAMonitorConfig(spec, resolvedSource);
  return { poolMax: integerAt(pool, "max", resolved.ref), idleTimeoutSeconds: integerAt(pool, "idleTimeoutSeconds", resolved.ref) };
}

export function releaseAMonitorMigrationArtifactGaps(spec: HwlabRuntimeLaneSpec, resolvedSource: MonitorSqliteSource): readonly MonitorLegacyArtifactGap[] {
  const { resolved, migration } = releaseAMonitorConfig(spec, resolvedSource);
  const gaps = arrayAt(migration, "legacyArtifactGaps", resolved.ref).map((item, index) => {
    if (!isRecord(item)) throw new Error(`${resolved.ref}.migration.legacyArtifactGaps[${index}] must be an object`);
    const gap = item;
    return { runId: textAt(gap, "runId", resolved.ref), artifactCount: integerAt(gap, "artifactCount", resolved.ref), stateDir: textAt(gap, "stateDir", resolved.ref), reportSha256: textAt(gap, "reportSha256", resolved.ref) };
  });
  const runIds = new Set<string>();
  for (const gap of gaps) {
    if (runIds.has(gap.runId)) throw new Error(`${resolved.ref}.migration.legacyArtifactGaps runId values must be unique`);
    if (gap.stateDir === "." || !gap.stateDir.startsWith(".") || gap.stateDir.includes("\\") || gap.stateDir.split("/").includes("..")) throw new Error(`${resolved.ref}.migration.legacyArtifactGaps stateDir must be a safe relative path`);
    if (!/^sha256:[a-f0-9]{64}$/u.test(gap.reportSha256)) throw new Error(`${resolved.ref}.migration.legacyArtifactGaps reportSha256 must be sha256:<64 lowercase hex>`);
    runIds.add(gap.runId);
  }
  return [...gaps].sort((left, right) => left.runId.localeCompare(right.runId));
}

function releaseAMonitorConfig(spec: HwlabRuntimeLaneSpec, resolvedSource: MonitorSqliteSource): { readonly resolved: ReturnType<typeof readWebProbeSentinelConfigRef>; readonly runtime: Record<string, unknown>; readonly migration: Record<string, unknown>; readonly job: Record<string, unknown>; readonly namespace: string; readonly deploymentName: string; readonly port: number; readonly secret: Record<string, unknown>; readonly pool: Record<string, unknown>; readonly labels: Record<string, string> } {
  const configRef = spec.observability.webProbe?.monitor?.configRef;
  if (!configRef) throw new Error(`lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.monitor.configRef is required`);
  const resolved = readWebProbeSentinelConfigRef(spec, configRef);
  if (!isRecord(resolved.target)) throw new Error(`${resolved.ref} must resolve to an object`);
  const monitor = resolved.target;
  const runtime = recordAt(monitor, "runtime", resolved.ref);
  const persistence = recordAt(monitor, "persistence", resolved.ref);
  const migration = recordAt(monitor, "migration", resolved.ref);
  const source = recordAt(migration, "source", resolved.ref);
  const job = recordAt(migration, "job", resolved.ref);
  const exposure = recordAt(monitor, "publicExposure", resolved.ref);
  const cicd = recordAt(monitor, "cicd", resolved.ref);
  const expectedRegistry = `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinels`;
  const expectedConsumer = `sentinel-${spec.nodeId.toLowerCase()}-${spec.lane}`;
  if (textAt(runtime, "node", resolved.ref) !== spec.nodeId || textAt(runtime, "lane", resolved.ref) !== spec.lane || textAt(persistence, "mode", resolved.ref) !== "host-postgres" || textAt(persistence, "authority", resolved.ref) !== "central-prepared-non-authoritative" || exposure.enabled !== false || textAt(exposure, "mode", resolved.ref) !== "prepared-not-routed" || textAt(exposure, "hostname", resolved.ref).length === 0 || textAt(cicd, "mode", resolved.ref) !== "existing-pac-consumer" || textAt(cicd, "consumer", resolved.ref) !== expectedConsumer || textAt(migration, "mode", resolved.ref) !== "legacy-authority") throw new Error(`${resolved.ref} must remain a non-authoritative Release A configuration`);
  if (textAt(source, "registry", resolved.ref) !== expectedRegistry || textAt(job, "workflow", resolved.ref) !== "export-import-verify" || job.readOnlySource !== true || typeof job.enabled !== "boolean" || !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(textAt(job, "name", resolved.ref))) throw new Error(`${resolved.ref}.migration must reference the resolved active registry and declare an export-import-verify read-only workflow`);
  const namespace = textAt(runtime, "namespace", resolved.ref);
  const deploymentName = textAt(runtime, "deploymentName", resolved.ref);
  const port = integerAt(runtime, "servicePort", resolved.ref);
  const secret = recordAt(runtime, "databaseSecret", resolved.ref);
  textAt(secret, "name", resolved.ref);
  textAt(secret, "key", resolved.ref);
  textAt(secret, "sourceRef", resolved.ref);
  const pool = recordAt(runtime, "pool", resolved.ref);
  return { resolved, runtime, migration, job, namespace, deploymentName, port, secret, pool, labels: { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/part-of": "hwlab-web-probe-monitor", "app.kubernetes.io/managed-by": "unidesk", "unidesk.ai/migration-mode": "legacy-authority" } };
}

function recordAt(value: Record<string, unknown>, key: string, path: string): Record<string, unknown> { if (!isRecord(value[key])) throw new Error(`${path}.${key} must be an object`); return value[key] as Record<string, unknown>; }
function arrayAt(value: Record<string, unknown>, key: string, path: string): readonly unknown[] { if (!Array.isArray(value[key])) throw new Error(`${path}.${key} must be an array`); return value[key] as readonly unknown[]; }
function textAt(value: Record<string, unknown>, key: string, path: string): string { const item = value[key]; if (typeof item !== "string" || item.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return item; }
function integerAt(value: Record<string, unknown>, key: string, path: string): number { const item = value[key]; if (typeof item !== "number" || !Number.isSafeInteger(item) || item < 1) throw new Error(`${path}.${key} must be a positive integer`); return item; }
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
function versionedJobName(base: string, imageRef: string, nameSuffix?: string): string { const imageSuffix = imageRef.split(":").at(-1)?.replace(/[^a-z0-9-]/gu, "-").slice(0, 20) || "image"; const suffix = nameSuffix === undefined ? imageSuffix : `${imageSuffix}-${nameSuffix.replace(/[^a-z0-9-]/gu, "-").slice(0, 12)}`; return `${base}-${suffix}`.slice(0, 63).replace(/-+$/u, ""); }
function requireImageRef(imageRef: string): void { if (typeof imageRef !== "string" || imageRef.trim().length === 0) throw new Error("Release A monitor imageRef must be a non-empty current PaC image reference"); }
