// scripts/library-decisions/schema.mjs
// Zod-validated schema for library decision trace files.
// Shared by: evaluate-library skill, pre-commit check, sandcastle reviewer.

import { z } from "zod";
import fs from "node:fs";

// ---- Zod schema ----

const filterResultsSchema = z
  .object({
    license: z.string().min(1),
    types: z.string().min(1),
    maintenance: z.enum(["active", "dormant", "abandoned"]),
    "boundary-fit": z.enum(["pass", "fail"]),
    "shadow-check": z.string().min(1),
    "eu-residency": z.enum(["ok", "n/a", "self-hostable", "fail"]),
    "cve-scan": z.string().min(1),
    "named-consumer": z.enum(["pass", "fail"]),
    socketRisk: z.union([z.literal("clean"), z.literal("flagged"), z.string()]),
  })
  .strict();

export const traceSchema = z
  .object({
    package: z.string().min(1),
    version: z.string().min(1),
    tier: z.enum(["app", "feature", "core"]),
    decision: z.enum(["approved", "rejected"]),
    date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD"),
    deciders: z.array(z.string()),
    adr: z.string().nullable(),
    lastRevalidated: z.string().nullable(),
    "filter-results": filterResultsSchema,
    "verification-commands": z.array(z.string()),
    "accepted-cves": z.array(z.string()).optional(),
  })
  .strict();

// ---- Helpers ----

/** Strip surrounding single- or double-quotes from a YAML scalar. */
function unquote(s) {
  if (
    (s.startsWith('"') && s.endsWith('"')) ||
    (s.startsWith("'") && s.endsWith("'"))
  ) {
    return s.slice(1, -1);
  }
  return s;
}

/** Parse a YAML inline flow array: `[a, "b", c]` → `["a", "b", "c"]`. */
function parseInlineArray(rawValue) {
  const inner = rawValue.slice(1, -1).trim();
  return inner === "" ? [] : inner.split(",").map((s) => unquote(s.trim()));
}

/**
 * Consume indented children (nested object or block-sequence array) starting
 * at `startIdx` in `lines`. Returns the parsed value and the index of the
 * first non-indented line (i.e. where the parent should resume scanning).
 */
function parseNestedBlock(lines, startIdx) {
  const nested = {};
  const arr = [];
  let i = startIdx;

  while (i < lines.length && /^ {2}/.test(lines[i])) {
    const child = lines[i];
    const itemMatch = child.match(/^ {2}- (.+)$/);
    if (itemMatch) {
      arr.push(unquote(itemMatch[1].trim()));
    } else {
      const nestedMatch = child.match(/^ {2}([\w-]+):\s*(.*)$/);
      if (nestedMatch) {
        nested[nestedMatch[1]] = unquote(nestedMatch[2].trim());
      }
    }
    i++;
  }

  return { value: arr.length > 0 ? arr : nested, nextIdx: i };
}

/**
 * Parse the YAML frontmatter of a markdown file into a plain JS object.
 * Handles the trace format: scalar values, inline flow arrays, one-level
 * nested objects, and block-sequence arrays.
 */
export function parseFrontmatter(text) {
  const match = text.match(/^---\n([\s\S]+?)\n---/);
  if (!match) throw new Error("No YAML frontmatter found");

  const lines = match[1].split("\n");
  const result = {};
  let i = 0;

  while (i < lines.length) {
    const line = lines[i];
    const topMatch = line.match(/^([\w-]+):\s*(.*)$/);
    if (!topMatch) {
      i++;
      continue;
    }

    const key = topMatch[1];
    const rawValue = topMatch[2].trim();

    if (rawValue === "") {
      const { value, nextIdx } = parseNestedBlock(lines, i + 1);
      result[key] = value;
      i = nextIdx;
    } else if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
      result[key] = parseInlineArray(rawValue);
      i++;
    } else {
      const v = unquote(rawValue);
      result[key] = v === "null" ? null : v;
      i++;
    }
  }

  return result;
}

// ---- Public API ----

/**
 * Validate an already-parsed frontmatter object against the trace schema.
 * Throws ZodError on failure; returns the parsed (typed) value on success.
 */
export function validateTrace(raw) {
  return traceSchema.parse(raw);
}

/**
 * Read a trace `.md` file, parse its frontmatter, and validate it.
 * Throws on missing frontmatter or schema violations.
 */
export function parseTrace(filePath) {
  const text = fs.readFileSync(filePath, "utf8");
  const raw = parseFrontmatter(text);
  return validateTrace(raw);
}
