// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// Responsibility: Resolve YAML-first web-probe sentinel registry entries into one selected sentinel config graph.
import { existsSync, readFileSync } from "node:fs";
import { rootPath } from "./config";
import { readWebProbeSentinelConfigRefTarget, type WebProbeSentinelTemplateContext } from "./hwlab-node-web-sentinel-config-ref";
import { HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeSentinelConfigRefKey, type HwlabRuntimeWebProbeSentinelRegistryItemSpec } from "./hwlab-node-lanes";
import { materializeYamlComposition } from "./yaml-composition";

export interface ResolvedWebProbeSentinel {
  readonly id: string;
  readonly enabled: boolean;
  readonly mode: "registry" | "legacy";
  readonly rootPath: string;
  readonly configRef: string | null;
  readonly configRefs: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
  readonly target: Record<string, unknown>;
}

export interface WebProbeSentinelRegistryRow {
  readonly id: string;
  readonly enabled: boolean;
  readonly configRef: string;
  readonly monitorRoot: boolean;
  readonly publicBaseUrl?: string;
  readonly routePrefix?: string;
}

export function webProbeSentinelRegistryRows(spec: HwlabRuntimeLaneSpec): readonly WebProbeSentinelRegistryRow[] {
  const registry = spec.observability.webProbe?.sentinels;
  if (registry !== undefined) return registry.map((item) => sentinelRegistryRow(spec, item.id, item.enabled, item.configRef));
  const legacy = spec.observability.webProbe?.sentinel;
  if (legacy === undefined) return [];
  return [sentinelRegistryRow(
    spec,
    "workbench-dsflash-go-tool-call-10x",
    legacy.enabled,
    `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel`,
  )];
}

export function effectiveWebProbeSentinelPublicExposure(spec: HwlabRuntimeLaneSpec, sentinelId: string, publicExposure: Record<string, unknown>): Record<string, unknown> {
  const monitorRoot = spec.observability.webProbe?.monitorRoot;
  if (monitorRoot === undefined || monitorRoot.sentinelId !== sentinelId) return publicExposure;
  if (!monitorRoot.enabled) {
    return {
      ...publicExposure,
      monitorRoot: {
        enabled: false,
        sentinelId,
        caddyManagedBlockOwner: monitorRoot.caddyManagedBlockOwner,
        valuesRedacted: true,
      },
    };
  }
  const caddy = isRecord(publicExposure.caddy) ? publicExposure.caddy : {};
  return {
    ...publicExposure,
    publicBaseUrl: monitorRoot.publicBaseUrl,
    routePrefix: monitorRoot.routePrefix,
    caddy: {
      ...caddy,
      managedBlockOwner: monitorRoot.caddyManagedBlockOwner,
      rootOrder: "active",
    },
    monitorRoot: {
      enabled: true,
      sentinelId,
      publicBaseUrl: monitorRoot.publicBaseUrl,
      routePrefix: monitorRoot.routePrefix,
      caddyManagedBlockOwner: monitorRoot.caddyManagedBlockOwner,
      valuesRedacted: true,
    },
  };
}

export function resolveWebProbeSentinel(spec: HwlabRuntimeLaneSpec, sentinelId: string | null | undefined): ResolvedWebProbeSentinel {
  const registry = spec.observability.webProbe?.sentinels;
  if (registry !== undefined) return resolveRegistrySentinel(spec, registry, sentinelId ?? null);
  const legacy = spec.observability.webProbe?.sentinel;
  if (legacy === undefined) {
    throw new Error(`config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinels is missing`);
  }
  const id = sentinelId ?? "workbench-dsflash-go-tool-call-10x";
  return {
    id,
    enabled: legacy.enabled,
    mode: "legacy",
    rootPath: `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel`,
    configRef: null,
    configRefs: legacy.configRefs,
    target: {
      id,
      configRefs: legacy.configRefs,
      valuesRedacted: true,
    },
  };
}

export function requireSentinelIdForRegistry(spec: HwlabRuntimeLaneSpec, sentinelId: string | null | undefined, command: string): void {
  const registry = spec.observability.webProbe?.sentinels;
  if (registry !== undefined && registry.length > 1 && (sentinelId === null || sentinelId === undefined || sentinelId.length === 0)) {
    const ids = registry.map((item) => item.id).join(", ");
    throw new Error(`${command} requires --sentinel <id> when multiple sentinels are declared; available: ${ids}`);
  }
}

function resolveRegistrySentinel(spec: HwlabRuntimeLaneSpec, registry: readonly HwlabRuntimeWebProbeSentinelRegistryItemSpec[], sentinelId: string | null): ResolvedWebProbeSentinel {
  const selected = sentinelId === null && registry.length === 1
    ? registry[0]
    : registry.find((item) => item.id === sentinelId);
  if (selected === undefined) {
    const ids = registry.map((item) => item.id).join(", ");
    throw new Error(`unknown web-probe sentinel ${sentinelId ?? "-"} for ${spec.nodeId}/${spec.lane}; available: ${ids}`);
  }
  const target = readConfigRefRecord(selected.configRef, spec);
  const targetId = optionalStringAt(target, "id") ?? selected.id;
  if (targetId !== selected.id) {
    throw new Error(`${selected.configRef}.id=${targetId} does not match registry id ${selected.id}`);
  }
  return {
    id: selected.id,
    enabled: selected.enabled,
    mode: "registry",
    rootPath: `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinels.${selected.id}`,
    configRef: selected.configRef,
    configRefs: normalizeSentinelConfigRefs(target, selected.configRef),
    target,
  };
}

function sentinelRegistryRow(spec: HwlabRuntimeLaneSpec, id: string, enabled: boolean, configRef: string): WebProbeSentinelRegistryRow {
  const monitorRoot = spec.observability.webProbe?.monitorRoot;
  const isMonitorRoot = monitorRoot?.enabled === true && monitorRoot.sentinelId === id;
  return {
    id,
    enabled,
    configRef,
    monitorRoot: isMonitorRoot,
    ...(isMonitorRoot ? { publicBaseUrl: monitorRoot.publicBaseUrl, routePrefix: monitorRoot.routePrefix } : {}),
  };
}

function normalizeSentinelConfigRefs(target: Record<string, unknown>, ref: string): Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string> {
  const rawRefs = recordAt(target, "configRefs");
  const normalized: Record<string, string> = {};
  for (const key of Object.keys(rawRefs)) {
    const value = rawRefs[key];
    if (typeof value === "string" && value.length > 0) normalized[key] = value;
  }
  if (normalized.scenarios === undefined && normalized.workflow !== undefined) normalized.scenarios = normalized.workflow;
  const missing = HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS.filter((key) => normalized[key] === undefined);
  if (missing.length > 0) throw new Error(`${ref}.configRefs is missing ${missing.join(",")}`);
  return Object.fromEntries(HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS.map((key) => [key, normalized[key]])) as Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
}

function readConfigRefRecord(ref: string, context: WebProbeSentinelTemplateContext): Record<string, unknown> {
  const target = readConfigRefTarget(ref, context);
  if (!isRecord(target)) throw new Error(`${ref} must point to a YAML object`);
  return target;
}

export function readConfigRefTarget(ref: string, context?: WebProbeSentinelTemplateContext): unknown {
  if (context !== undefined) return readWebProbeSentinelConfigRefTarget(context, ref);
  const [file, path, extra] = ref.split("#");
  if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
    throw new Error(`${ref} must use path/to/file.yaml#object.path syntax`);
  }
  if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
    throw new Error(`${ref} must reference a repo-relative config/*.yaml file without ..`);
  }
  const absPath = rootPath(file);
  if (!existsSync(absPath)) throw new Error(`${file} does not exist`);
  const doc = materializeYamlComposition(Bun.YAML.parse(readFileSync(absPath, "utf8")) as unknown, { label: file }).value;
  return valueAtPath(doc, path);
}

function recordAt(value: unknown, path: string): Record<string, unknown> {
  const found = valueAtPath(value, path);
  if (!isRecord(found)) throw new Error(`${path} must be an object`);
  return found;
}

function optionalStringAt(value: unknown, path: string): string | null {
  const found = valueAtPath(value, path);
  return typeof found === "string" && found.length > 0 ? found : null;
}

function valueAtPath(value: unknown, path: string): unknown {
  let current: unknown = value;
  for (const segment of path.split(".")) {
    if (segment.length === 0) return undefined;
    const match = /^(?:([A-Za-z0-9_-]+))?(?:\[(\d+)\])?$/u.exec(segment);
    if (match === null) return undefined;
    if (match[1] !== undefined) {
      if (!isRecord(current)) return undefined;
      current = current[match[1]];
    }
    if (match[2] !== undefined) {
      if (!Array.isArray(current)) return undefined;
      current = current[Number(match[2])];
    }
  }
  return current;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
