// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-29-p14-yaml-template-refs.
// Responsibility: Render YAML-first web-probe sentinel configRefs with node/lane variables and selector paths.
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { rootPath } from "./config";
import { materializeYamlComposition } from "./yaml-composition";

export interface WebProbeSentinelTemplateContext {
  readonly nodeId: string;
  readonly lane: string;
  readonly runtimeNamespace?: string;
}

type TemplateVarValue = string | number | boolean;

export interface WebProbeSentinelRenderedConfigRef {
  readonly originalRef: string;
  readonly ref: string;
  readonly file: string;
  readonly path: string;
  readonly target: unknown;
  readonly sha256: string;
  readonly byteCount: number;
  readonly variableNames: readonly string[];
}

export function readWebProbeSentinelConfigRefTarget(context: WebProbeSentinelTemplateContext, ref: string): unknown {
  return readWebProbeSentinelConfigRef(context, ref).target;
}

export function readWebProbeSentinelConfigRef(context: WebProbeSentinelTemplateContext, ref: string): WebProbeSentinelRenderedConfigRef {
  const builtinVars = templateContextVars(context);
  const renderedRef = renderTemplateString(ref, builtinVars, "configRef");
  const parsed = parseRenderedConfigRef(renderedRef);
  const absPath = rootPath(parsed.file);
  if (!existsSync(absPath)) throw new Error(`${parsed.file} does not exist`);
  const text = readFileSync(absPath, "utf8");
  const doc = materializeYamlComposition(Bun.YAML.parse(text) as unknown, { label: parsed.file }).value;
  const vars = { ...builtinVars, ...documentVars(doc, builtinVars) };
  const path = renderTemplateString(parsed.path, vars, `${parsed.file}#path`);
  const target = valueAtConfigPath(doc, path);
  if (target === undefined) throw new Error(`${parsed.file}#${path} is missing`);
  return {
    originalRef: ref,
    ref: `${parsed.file}#${path}`,
    file: parsed.file,
    path,
    target: renderTemplateValue(target, vars),
    sha256: `sha256:${createHash("sha256").update(text).digest("hex")}`,
    byteCount: Buffer.byteLength(text),
    variableNames: Object.keys(vars).sort(),
  };
}

export function renderWebProbeSentinelConfigRefString(context: WebProbeSentinelTemplateContext, ref: string): string {
  return renderTemplateString(ref, templateContextVars(context), "configRef");
}

function templateContextVars(context: WebProbeSentinelTemplateContext): Record<string, TemplateVarValue> {
  const node = context.nodeId;
  const nodeLower = node.toLowerCase();
  const lane = context.lane;
  const laneMinor = lane.replace(/^v/u, "");
  return {
    NODE: node,
    node,
    nodeLower,
    NODE_LOWER: nodeLower,
    LANE: lane,
    lane,
    laneMinor,
    LANE_MINOR: laneMinor,
    nodeLane: `${nodeLower}-${lane}`,
    NODE_LANE: `${node}-${lane}`,
    runtimeNamespace: context.runtimeNamespace ?? "",
    ...contextTemplateVars(context),
  };
}

function contextTemplateVars(context: WebProbeSentinelTemplateContext): Record<string, TemplateVarValue> {
  const observability = isRecord((context as { readonly observability?: unknown }).observability)
    ? (context as { readonly observability: Record<string, unknown> }).observability
    : {};
  const webProbe = isRecord(observability.webProbe) ? observability.webProbe : {};
  const templateVars = isRecord(webProbe.templateVars) ? webProbe.templateVars : {};
  const result: Record<string, TemplateVarValue> = {};
  for (const [key, value] of Object.entries(templateVars)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`observability.webProbe.templateVars.${key} is not a supported variable name`);
    if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
      throw new Error(`observability.webProbe.templateVars.${key} must be a scalar`);
    }
    result[key] = value;
  }
  return result;
}

function documentVars(doc: unknown, vars: Record<string, TemplateVarValue>): Record<string, TemplateVarValue> {
  if (!isRecord(doc) || !isRecord(doc.vars)) return {};
  const result: Record<string, TemplateVarValue> = {};
  for (const [key, value] of Object.entries(doc.vars)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`vars.${key} is not a supported variable name`);
    if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
      throw new Error(`vars.${key} must be a scalar`);
    }
    result[key] = typeof value === "string"
      ? renderTemplateString(value, { ...vars, ...result }, `vars.${key}`)
      : value;
  }
  return result;
}

function renderTemplateValue(value: unknown, vars: Record<string, TemplateVarValue>): unknown {
  if (typeof value === "string") return renderTemplateScalar(value, vars, "YAML string");
  if (Array.isArray(value)) return value.map((item) => renderTemplateValue(item, vars));
  if (!isRecord(value)) return value;
  const localVars = localTemplateVars(value, vars);
  const rendered: Record<string, unknown> = {};
  for (const [key, item] of Object.entries(value)) {
    if (key === "vars") continue;
    rendered[key] = renderTemplateValue(item, localVars);
  }
  return rendered;
}

function localTemplateVars(value: Record<string, unknown>, parentVars: Record<string, TemplateVarValue>): Record<string, TemplateVarValue> {
  if (!isRecord(value.vars)) return parentVars;
  const local: Record<string, TemplateVarValue> = {};
  for (const [key, item] of Object.entries(value.vars)) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`vars.${key} is not a supported variable name`);
    if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") throw new Error(`vars.${key} must be a scalar`);
    local[key] = typeof item === "string"
      ? renderTemplateString(item, { ...parentVars, ...local }, `vars.${key}`)
      : item;
  }
  return { ...parentVars, ...local };
}

function renderTemplateScalar(value: string, vars: Record<string, TemplateVarValue>, label: string): TemplateVarValue {
  const whole = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/u.exec(value);
  if (whole !== null) {
    const rendered = vars[whole[1]];
    if (rendered === undefined) throw new Error(`${label} references undefined variable ${whole[1]}`);
    return rendered;
  }
  return renderTemplateString(value, vars, label);
}

function renderTemplateString(value: string, vars: Record<string, TemplateVarValue>, label: string): string {
  return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_match, key: string) => {
    const rendered = vars[key];
    if (rendered === undefined) throw new Error(`${label} references undefined variable ${key}`);
    return String(rendered);
  });
}

function parseRenderedConfigRef(ref: string): { readonly file: string; readonly path: string } {
  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 ..`);
  }
  return { file, path };
}

function valueAtConfigPath(value: unknown, path: string): unknown {
  let current = value;
  for (const segment of path.split(".")) {
    if (segment.length === 0) return undefined;
    current = valueAtSegment(current, segment);
    if (current === undefined) return undefined;
  }
  return current;
}

function valueAtSegment(value: unknown, segment: string): unknown {
  let rest = segment;
  let current = value;
  const key = /^[A-Za-z0-9_-]+/u.exec(rest)?.[0] ?? null;
  if (key !== null) {
    if (!isRecord(current)) return undefined;
    current = current[key];
    rest = rest.slice(key.length);
  }
  if (key === null && !rest.startsWith("[")) return undefined;
  while (rest.length > 0) {
    const match = /^\[([^\]]+)\]/u.exec(rest);
    if (match === null) return undefined;
    current = valueAtBracket(current, match[1]);
    if (current === undefined) return undefined;
    rest = rest.slice(match[0].length);
  }
  return current;
}

function valueAtBracket(value: unknown, selector: string): unknown {
  if (/^\d+$/u.test(selector)) {
    if (!Array.isArray(value)) return undefined;
    return value[Number(selector)];
  }
  const match = /^([A-Za-z0-9_-]+)=([A-Za-z0-9_.:/@+-]+)$/u.exec(selector);
  if (match === null || !Array.isArray(value)) return undefined;
  const [, key, expected] = match;
  return value.find((item) => isRecord(item) && item[key] === expected);
}

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