// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// Responsibility: shared YAML configRef parsing and redacted reference summaries.
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { rootPath } from "../config";

export interface ParsedConfigRef {
  ref: string;
  file: string;
  fragment: string;
}

export interface ResolvedConfigRef {
  ref: string;
  file: string;
  fragment: string;
  value: unknown;
  valueType: string;
  present: true;
  sha256: string;
  summary: unknown;
}

export function parseConfigRef(ref: string, label = "configRef"): ParsedConfigRef {
  const [file, fragment, extra] = ref.split("#");
  if (extra !== undefined || file === undefined || fragment === undefined || file.length === 0 || fragment.length === 0) {
    throw new Error(`${label} 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(`${label} must reference a repo-relative config/*.yaml file without ..`);
  }
  if (!/^[A-Za-z0-9_.\-[\]]+$/u.test(fragment)) throw new Error(`${label} has an unsupported YAML path fragment`);
  return { ref, file, fragment };
}

export function resolveConfigRef(ref: string, label = "configRef"): ResolvedConfigRef {
  const parsed = parseConfigRef(ref, label);
  const yaml = Bun.YAML.parse(readFileSync(rootPath(parsed.file), "utf8")) as unknown;
  const value = valueAtPath(yaml, parsed.fragment, `${parsed.file}#${parsed.fragment}`);
  const serialized = stableSerialize(value);
  return {
    ...parsed,
    value,
    valueType: valueType(value),
    present: true,
    sha256: createHash("sha256").update(serialized).digest("hex"),
    summary: summarizeConfigValue(value),
  };
}

export function resolveConfigRefString(ref: string, label = "configRef"): string {
  const resolved = resolveConfigRef(ref, label);
  if (typeof resolved.value !== "string" || resolved.value.length === 0) throw new Error(`${resolved.file}#${resolved.fragment} must resolve to a non-empty string`);
  return resolved.value;
}

export function configRefSummary(ref: string, label = "configRef"): Record<string, unknown> {
  const resolved = resolveConfigRef(ref, label);
  return {
    ref: resolved.ref,
    file: resolved.file,
    path: resolved.fragment,
    present: resolved.present,
    valueType: resolved.valueType,
    sha256: resolved.sha256,
    summary: resolved.summary,
  };
}

export function configRefGraph(refs: Array<{ id: string; ref: string }>): Array<Record<string, unknown>> {
  return refs.map((item) => ({ id: item.id, ...configRefSummary(item.ref, item.id) }));
}

function valueAtPath(value: unknown, fragment: string, label: string): unknown {
  let current = value;
  for (const segment of fragment.split(".")) {
    if (segment.length === 0) throw new Error(`${label} has an empty path segment`);
    const parts = [...segment.matchAll(/([A-Za-z0-9_-]+)|\[(\d+)\]/gu)];
    if (parts.length === 0 || parts.map((match) => match[0]).join("") !== segment) throw new Error(`${label} has unsupported segment ${segment}`);
    for (const part of parts) {
      const key = part[1];
      const index = part[2];
      if (key !== undefined) {
        if (typeof current !== "object" || current === null || Array.isArray(current)) throw new Error(`${label} segment ${key} does not resolve through an object`);
        const record = current as Record<string, unknown>;
        if (!Object.prototype.hasOwnProperty.call(record, key)) throw new Error(`${label} is missing segment ${key}`);
        current = record[key];
      } else if (index !== undefined) {
        if (!Array.isArray(current)) throw new Error(`${label} segment [${index}] does not resolve through an array`);
        const parsed = Number(index);
        if (current[parsed] === undefined) throw new Error(`${label} is missing array item [${index}]`);
        current = current[parsed];
      }
    }
  }
  return current;
}

function valueType(value: unknown): string {
  if (Array.isArray(value)) return "array";
  if (value === null) return "null";
  return typeof value;
}

function summarizeConfigValue(value: unknown): unknown {
  if (typeof value === "string") return value.length > 160 ? `${value.slice(0, 157)}...` : value;
  if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
  if (Array.isArray(value)) return { kind: "array", count: value.length };
  if (typeof value === "object") {
    const keys = Object.keys(value as Record<string, unknown>);
    return { kind: "object", keys: keys.slice(0, 12), omittedKeys: Math.max(0, keys.length - 12) };
  }
  return String(value);
}

function stableSerialize(value: unknown): string {
  if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`;
  if (typeof value === "object" && value !== null) {
    return `{${Object.keys(value as Record<string, unknown>)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${stableSerialize((value as Record<string, unknown>)[key])}`)
      .join(",")}}`;
  }
  return JSON.stringify(value);
}
