// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config module for scripts/src/platform-infra-observability.ts.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.

// Moved mechanically from scripts/src/platform-infra-observability.ts:370-514 for #903.

// SPEC: PJ2026-01060501 OTel追踪 draft-2026-06-19-p0.
// Responsibility: YAML-first platform-infra OpenTelemetry tracing control commands.
import { Buffer } from "node:buffer";
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import {
  compactCapture,
  createYamlFieldReader,
  numberField,
  parseJsonOutput,
  redactSensitiveUnknown,
  shQuote,
  capture,
} from "../platform-infra-ops-library";

import type { ImageSpec, ObservabilityConfig, ObservabilityTarget, OtlpPorts, ServiceConnection } from "./types";
import { assertKnownEnabledTarget, parseStatusEndpoint } from "./actions";
import { apiPathField, arrayOfRecords, asRecord, booleanField, configFile, configLabel, enumField, integerField, kubernetesNameField, numberArrayField, objectField, portField, stringArrayField, stringField } from "./types";
import { configRefGraph, resolveConfigRefString } from "../ops/config-refs";

export function readObservabilityConfig(): ObservabilityConfig {
  const parsed = Bun.YAML.parse(readFileSync(configFile, "utf8")) as unknown;
  const root = asRecord(parsed, configLabel);
  const version = integerField(root, "version", "");
  const kind = stringField(root, "kind", "");
  if (kind !== "platform-infra-observability") throw new Error(`${configLabel}.kind must be platform-infra-observability`);
  const metadata = objectField(root, "metadata", "");
  const defaults = objectField(root, "defaults", "");
  const images = objectField(root, "images", "");
  const collector = objectField(root, "collector", "");
  const collectorOtlp = objectField(collector, "otlp", "collector");
  const traceBackend = objectField(root, "traceBackend", "");
  const traceBackendOtlp = objectField(traceBackend, "otlp", "traceBackend");
  const traceBackendStorage = objectField(traceBackend, "storage", "traceBackend");
  const metricsBackend = objectField(root, "metricsBackend", "");
  const metricsStorage = objectField(metricsBackend, "storage", "metricsBackend");
  const metricsDiscovery = objectField(metricsBackend, "discovery", "metricsBackend");
  const metricsLabelSelector = objectField(metricsDiscovery, "labelSelector", "metricsBackend.discovery");
  const metricsAnnotations = objectField(metricsDiscovery, "annotationKeys", "metricsBackend.discovery");
  const metricsQuery = objectField(metricsBackend, "query", "metricsBackend");
  const sampling = objectField(root, "sampling", "");
  const instrumentation = objectField(root, "instrumentation", "");
  const resourceAttributes = objectField(root, "resourceAttributes", "");
  const probes = objectField(root, "probes", "");
  const config: ObservabilityConfig = {
    version,
    kind,
    metadata: {
      id: stringField(metadata, "id", "metadata"),
      owner: stringField(metadata, "owner", "metadata"),
      spec: stringField(metadata, "spec", "metadata"),
      relatedIssues: numberArrayField(metadata, "relatedIssues", "metadata"),
    },
    defaults: { targetId: stringField(defaults, "targetId", "defaults") },
    images: {
      collector: imageSpec(objectField(images, "collector", "images"), "images.collector"),
      tempo: imageSpec(objectField(images, "tempo", "images"), "images.tempo"),
      prometheus: imageSpec(objectField(images, "prometheus", "images"), "images.prometheus"),
    },
    targets: arrayOfRecords(root.targets, "targets").map(parseTarget),
    collector: {
      deploymentName: kubernetesNameField(collector, "deploymentName", "collector"),
      serviceName: kubernetesNameField(collector, "serviceName", "collector"),
      configMapName: kubernetesNameField(collector, "configMapName", "collector"),
      replicas: integerField(collector, "replicas", "collector"),
      healthPort: portField(collector, "healthPort", "collector"),
      otlp: parseOtlpPorts(collectorOtlp, "collector.otlp"),
    },
    traceBackend: {
      type: enumField(traceBackend, "type", "traceBackend", ["tempo"] as const),
      deploymentName: kubernetesNameField(traceBackend, "deploymentName", "traceBackend"),
      serviceName: kubernetesNameField(traceBackend, "serviceName", "traceBackend"),
      configMapName: kubernetesNameField(traceBackend, "configMapName", "traceBackend"),
      replicas: integerField(traceBackend, "replicas", "traceBackend"),
      httpPort: portField(traceBackend, "httpPort", "traceBackend"),
      otlp: parseOtlpPorts(traceBackendOtlp, "traceBackend.otlp"),
      storage: {
        mode: enumField(traceBackendStorage, "mode", "traceBackend.storage", ["emptyDir"] as const),
        retention: stringField(traceBackendStorage, "retention", "traceBackend.storage"),
      },
    },
    metricsBackend: {
      type: enumField(metricsBackend, "type", "metricsBackend", ["prometheus"] as const),
      enabled: booleanField(metricsBackend, "enabled", "metricsBackend"),
      targetIds: stringArrayField(metricsBackend, "targetIds", "metricsBackend"),
      deploymentName: kubernetesNameField(metricsBackend, "deploymentName", "metricsBackend"),
      serviceName: kubernetesNameField(metricsBackend, "serviceName", "metricsBackend"),
      configMapName: kubernetesNameField(metricsBackend, "configMapName", "metricsBackend"),
      serviceAccountName: kubernetesNameField(metricsBackend, "serviceAccountName", "metricsBackend"),
      clusterRoleName: kubernetesNameField(metricsBackend, "clusterRoleName", "metricsBackend"),
      clusterRoleBindingName: kubernetesNameField(metricsBackend, "clusterRoleBindingName", "metricsBackend"),
      persistentVolumeClaimName: kubernetesNameField(metricsBackend, "persistentVolumeClaimName", "metricsBackend"),
      replicas: integerField(metricsBackend, "replicas", "metricsBackend"),
      httpPort: portField(metricsBackend, "httpPort", "metricsBackend"),
      storage: {
        mode: enumField(metricsStorage, "mode", "metricsBackend.storage", ["persistentVolumeClaim"] as const),
        size: stringField(metricsStorage, "size", "metricsBackend.storage"),
        retention: stringField(metricsStorage, "retention", "metricsBackend.storage"),
      },
      discovery: {
        namespaces: stringArrayField(metricsDiscovery, "namespaces", "metricsBackend.discovery"),
        labelSelector: {
          key: stringField(metricsLabelSelector, "key", "metricsBackend.discovery.labelSelector"),
          value: stringField(metricsLabelSelector, "value", "metricsBackend.discovery.labelSelector"),
        },
        annotationKeys: {
          scrape: stringField(metricsAnnotations, "scrape", "metricsBackend.discovery.annotationKeys"),
          path: stringField(metricsAnnotations, "path", "metricsBackend.discovery.annotationKeys"),
          port: stringField(metricsAnnotations, "port", "metricsBackend.discovery.annotationKeys"),
        },
        defaultPath: apiPathField(metricsDiscovery, "defaultPath", "metricsBackend.discovery"),
        scrapeInterval: stringField(metricsDiscovery, "scrapeInterval", "metricsBackend.discovery"),
        scrapeTimeout: stringField(metricsDiscovery, "scrapeTimeout", "metricsBackend.discovery"),
      },
      query: {
        path: apiPathField(metricsQuery, "path", "metricsBackend.query"),
        timeoutSeconds: integerField(metricsQuery, "timeoutSeconds", "metricsBackend.query"),
        maxSeries: integerField(metricsQuery, "maxSeries", "metricsBackend.query"),
        maxResponseBytes: integerField(metricsQuery, "maxResponseBytes", "metricsBackend.query"),
        maxCloseoutQueries: integerField(metricsQuery, "maxCloseoutQueries", "metricsBackend.query"),
      },
    },
    sampling: {
      mode: enumField(sampling, "mode", "sampling", ["parentbased_traceidratio"] as const),
      ratio: numberField(sampling, "ratio", "sampling"),
    },
    instrumentation: {
      contextPropagation: stringArrayField(instrumentation, "contextPropagation", "instrumentation"),
      serviceConnections: arrayOfRecords(instrumentation.serviceConnections, "instrumentation.serviceConnections").map(parseServiceConnection),
    },
    resourceAttributes: {
      required: stringArrayField(resourceAttributes, "required", "resourceAttributes"),
      businessCorrelationAttributes: stringArrayField(resourceAttributes, "businessCorrelationAttributes", "resourceAttributes"),
    },
    probes: {
      readinessPath: apiPathField(probes, "readinessPath", "probes"),
      traceQueryPathTemplate: stringField(probes, "traceQueryPathTemplate", "probes"),
      statusEndpoints: arrayOfRecords(probes.statusEndpoints, "probes.statusEndpoints").map(parseStatusEndpoint),
    },
  };
  if (config.targets.length === 0) throw new Error(`${configLabel}.targets must not be empty`);
  assertKnownEnabledTarget(config.targets, config.defaults.targetId, "defaults.targetId");
  for (const targetId of config.metricsBackend.targetIds) assertKnownEnabledTarget(config.targets, targetId, "metricsBackend.targetIds");
  if (config.metricsBackend.enabled && config.metricsBackend.targetIds.length === 0) throw new Error(`${configLabel}.metricsBackend.targetIds must not be empty when metricsBackend.enabled is true`);
  if (new Set(config.metricsBackend.targetIds.map((targetId) => targetId.toLowerCase())).size !== config.metricsBackend.targetIds.length) throw new Error(`${configLabel}.metricsBackend.targetIds must not contain duplicates`);
  if (!isKubernetesQualifiedName(config.metricsBackend.discovery.labelSelector.key)) throw new Error(`${configLabel}.metricsBackend.discovery.labelSelector.key must be a Kubernetes qualified name`);
  if (!isKubernetesLabelValue(config.metricsBackend.discovery.labelSelector.value)) throw new Error(`${configLabel}.metricsBackend.discovery.labelSelector.value must be a Kubernetes label value`);
  if (config.collector.replicas < 0 || config.traceBackend.replicas < 0 || config.metricsBackend.replicas < 0) throw new Error(`${configLabel} replicas must be >= 0`);
  if (config.metricsBackend.query.timeoutSeconds < 1 || config.metricsBackend.query.maxSeries < 1 || config.metricsBackend.query.maxResponseBytes < 1024 || config.metricsBackend.query.maxCloseoutQueries < 1) throw new Error(`${configLabel}.metricsBackend.query budgets must be positive and maxResponseBytes must be >= 1024`);
  if (config.sampling.ratio < 0 || config.sampling.ratio > 1) throw new Error(`${configLabel}.sampling.ratio must be between 0 and 1`);
  if (!config.probes.traceQueryPathTemplate.includes("{{traceId}}")) throw new Error(`${configLabel}.probes.traceQueryPathTemplate must include {{traceId}}`);
  return config;
}

export function isKubernetesQualifiedName(value: string): boolean {
  const slash = value.indexOf("/");
  if (slash !== value.lastIndexOf("/")) return false;
  const prefix = slash < 0 ? null : value.slice(0, slash);
  const name = slash < 0 ? value : value.slice(slash + 1);
  if (!isKubernetesNamePart(name)) return false;
  if (prefix === null) return true;
  if (prefix.length === 0 || prefix.length > 253) return false;
  return prefix.split(".").every((segment) => segment.length <= 63 && /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(segment));
}

export function isKubernetesLabelValue(value: string): boolean {
  return value.length <= 63 && (value === "" || isKubernetesNamePart(value));
}

function isKubernetesNamePart(value: string): boolean {
  return value.length > 0 && value.length <= 63 && /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value);
}

export function imageSpec(record: Record<string, unknown>, path: string): ImageSpec {
  const image = {
    repository: stringField(record, "repository", path),
    tag: stringField(record, "tag", path),
    pullPolicy: enumField(record, "pullPolicy", path, ["Always", "IfNotPresent", "Never"] as const),
  };
  if (!/^[A-Za-z0-9._/:@-]+$/u.test(`${image.repository}:${image.tag}`)) throw new Error(`${configLabel}.${path} must render a valid image reference`);
  return image;
}

export function parseTarget(record: Record<string, unknown>, index: number): ObservabilityTarget {
  const path = `targets[${index}]`;
  return {
    id: stringField(record, "id", path),
    route: stringField(record, "route", path),
    namespace: kubernetesNameField(record, "namespace", path),
    role: enumField(record, "role", path, ["active", "standby"] as const),
    enabled: booleanField(record, "enabled", path),
    createNamespace: booleanField(record, "createNamespace", path),
  };
}

export function parseOtlpPorts(record: Record<string, unknown>, path: string): OtlpPorts {
  return {
    grpcPort: portField(record, "grpcPort", path),
    httpPort: portField(record, "httpPort", path),
  };
}

export function parseServiceConnection(record: Record<string, unknown>, index: number): ServiceConnection {
  const path = `instrumentation.serviceConnections[${index}]`;
  const refs = record.configRefs === undefined ? null : objectField(record, "configRefs", path);
  const configRefs = refs === null
    ? {}
    : {
        targetNode: stringField(refs, "targetNode", `${path}.configRefs`),
        lane: stringField(refs, "lane", `${path}.configRefs`),
        namespace: stringField(refs, "namespace", `${path}.configRefs`),
      };
  return {
    serviceName: stringField(record, "serviceName", path),
    owningRepo: stringField(record, "owningRepo", path),
    targetNode: refs === null ? stringField(record, "targetNode", path) : resolveConfigRefString(configRefs.targetNode, `${path}.configRefs.targetNode`),
    lane: refs === null ? stringField(record, "lane", path) : resolveConfigRefString(configRefs.lane, `${path}.configRefs.lane`),
    namespace: refs === null ? kubernetesNameField(record, "namespace", path) : kubernetesNameValue(resolveConfigRefString(configRefs.namespace, `${path}.configRefs.namespace`), `${path}.configRefs.namespace`),
    configRefs,
    configRefGraph: refs === null
      ? []
      : configRefGraph([
          { id: `${path}.targetNode`, ref: configRefs.targetNode },
          { id: `${path}.lane`, ref: configRefs.lane },
          { id: `${path}.namespace`, ref: configRefs.namespace },
        ]),
    requiredSpans: stringArrayField(record, "requiredSpans", path),
  };
}

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