/**
 * Ratify — the binding half of accumulation (slice 2 of fix #1).
 *
 * Slice 1 binds *drafts* to the rules. Ratify binds the *rules* (`rules.ts`) to
 * the human source of truth (`maskinrommet/docs/skrivekontrakt.md`). Without it,
 * accumulation rots silently: add a rule to `rules.ts` and forget the contract
 * row (the gate enforces something undocumented), or codify a contract rule and
 * forget the gate entry (a "rule" nobody enforces). Either way the two drift.
 *
 * The contract carries a machine-readable "gate-bindings-manifest" — one row per
 * mechanical rule (id · ref · severity · provenance) — fenced between
 * `<!-- ratify:begin -->` and `<!-- ratify:end -->`. Ratify asserts a BIJECTION
 * between that manifest and `RULES`, matching ref + severity exactly, and that
 * every cited §-anchor resolves to a real section. It imports RULES directly
 * (not a text-parse of rules.ts) so the gate's own loader is the source.
 *
 * Pure + deterministic: no AI, no network. Exit 0 = in sync, 1 = drift.
 */

import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import type { Rule, Severity } from "./types.js";
import { RULES } from "./rules.js";

export interface ManifestRow {
  id: string;
  ref: string;
  severity: string;
  provenance: string;
}

export type RatifyIssueKind =
  | "missing-from-manifest" // rule in RULES, no manifest row
  | "extra-in-manifest" // manifest row, no rule in RULES
  | "ref-mismatch" // shared id, ref differs
  | "severity-mismatch" // shared id, severity differs
  | "dangling-ref"; // manifest/rule ref cites a §-anchor the contract lacks

export interface RatifyIssue {
  kind: RatifyIssueKind;
  id: string;
  detail: string;
}

export interface RatifyResult {
  contractPath: string | null;
  ruleCount: number;
  manifestCount: number;
  issues: RatifyIssue[];
  ok: boolean;
}

/** Default contract path, resolved relative to THIS module (cwd-independent). */
export function defaultContractPath(): string {
  const here = dirname(fileURLToPath(import.meta.url));
  // src → contract-gate → scripts → linkedin-studio → ktg-plugin-marketplace → repos
  return resolve(here, "../../../../../maskinrommet/docs/skrivekontrakt.md");
}

/**
 * Extract the manifest rows fenced by `<!-- ratify:begin/end -->`.
 * Returns null if the fence is missing (a hard error — the contract must carry
 * the manifest for the binding to exist at all).
 */
export function parseManifest(contractText: string): ManifestRow[] | null {
  const begin = contractText.indexOf("<!-- ratify:begin -->");
  const end = contractText.indexOf("<!-- ratify:end -->");
  if (begin === -1 || end === -1 || end < begin) return null;

  const block = contractText.slice(begin, end);
  const rows: ManifestRow[] = [];
  for (const raw of block.split("\n")) {
    const line = raw.trim();
    if (!line.startsWith("|")) continue;
    // cells between the outer pipes
    const cells = line.split("|").slice(1, -1).map((c) => c.trim());
    if (cells.length < 4) continue;
    const [id, ref, severity] = cells;
    // skip the header row and the |---|---| separator
    if (id === "rule-id" || /^:?-{2,}:?$/.test(id)) continue;
    if (!id) continue;
    rows.push({ id, ref, severity, provenance: cells[3] });
  }
  return rows;
}

/** Section anchors the contract actually defines, for dangling-ref checks. */
interface Anchors {
  hasC1: boolean;
  hasC2: boolean;
  hasB: boolean;
  bRowCount: number;
}

function contractAnchors(contractText: string): Anchors {
  const hasC1 = /^##\s+C1\./m.test(contractText);
  const hasC2 = /^##\s+C2\./m.test(contractText);
  const hasB = /^##\s+B\./m.test(contractText);
  // §B rule rows look like:  | 7 | **Regel** | … |
  let bRowCount = 0;
  for (const line of contractText.split("\n")) {
    if (/^\|\s*\d+\s*\|/.test(line)) bRowCount++;
  }
  return { hasC1, hasC2, hasB, bRowCount };
}

/** Validate that every §-anchor in a ref resolves to a real contract section. */
function danglingRefIssues(id: string, ref: string, anchors: Anchors): RatifyIssue[] {
  const out: RatifyIssue[] = [];
  // §B-3, §C1, §C2 — ignore non-§ tokens like "hygiene" / "★"
  const tokens = ref.match(/§[A-Za-z]+\d*(?:-\d+)?/g) ?? [];
  for (const tok of tokens) {
    const bRule = tok.match(/^§B-(\d+)$/);
    if (tok === "§C1") {
      if (!anchors.hasC1) out.push({ kind: "dangling-ref", id, detail: `${ref}: §C1 mangler i kontrakten` });
    } else if (tok === "§C2") {
      if (!anchors.hasC2) out.push({ kind: "dangling-ref", id, detail: `${ref}: §C2 mangler i kontrakten` });
    } else if (bRule) {
      const n = Number(bRule[1]);
      if (!anchors.hasB) out.push({ kind: "dangling-ref", id, detail: `${ref}: §B mangler i kontrakten` });
      else if (n > anchors.bRowCount)
        out.push({ kind: "dangling-ref", id, detail: `${ref}: §B-${n} finnes ikke (§B har ${anchors.bRowCount} regler)` });
    } else if (tok === "§B" && !anchors.hasB) {
      out.push({ kind: "dangling-ref", id, detail: `${ref}: §B mangler i kontrakten` });
    }
  }
  return out;
}

/** Core: compare RULES against a parsed manifest + contract anchors. */
export function ratify(contractText: string, rules: Rule[] = RULES): RatifyResult {
  const issues: RatifyIssue[] = [];
  const manifest = parseManifest(contractText);

  if (manifest === null) {
    return {
      contractPath: null,
      ruleCount: rules.length,
      manifestCount: 0,
      issues: [
        {
          kind: "missing-from-manifest",
          id: "(manifest)",
          detail:
            "Fant ikke gate-bindings-manifestet (<!-- ratify:begin/end -->) i §E. Kontrakten må bære manifestet for at bindingen skal eksistere.",
        },
      ],
      ok: false,
    };
  }

  const anchors = contractAnchors(contractText);
  const ruleById = new Map<string, Rule>(rules.map((r) => [r.id, r]));
  const manById = new Map<string, ManifestRow>();
  for (const row of manifest) {
    if (manById.has(row.id)) {
      issues.push({ kind: "extra-in-manifest", id: row.id, detail: "duplisert manifest-rad" });
      continue;
    }
    manById.set(row.id, row);
  }

  // RULES → manifest (every enforced rule must be documented)
  for (const rule of rules) {
    const row = manById.get(rule.id);
    if (!row) {
      issues.push({
        kind: "missing-from-manifest",
        id: rule.id,
        detail: `rules.ts håndhever «${rule.id}» (${rule.ref}, ${rule.severity}) uten en §E-manifest-rad`,
      });
      continue;
    }
    if (row.ref !== rule.ref) {
      issues.push({ kind: "ref-mismatch", id: rule.id, detail: `manifest «${row.ref}» ≠ rules.ts «${rule.ref}»` });
    }
    if (row.severity !== (rule.severity as Severity)) {
      issues.push({
        kind: "severity-mismatch",
        id: rule.id,
        detail: `manifest «${row.severity}» ≠ rules.ts «${rule.severity}»`,
      });
    }
    issues.push(...danglingRefIssues(rule.id, rule.ref, anchors));
  }

  // manifest → RULES (every documented rule must be enforced)
  for (const row of manifest) {
    if (!ruleById.has(row.id)) {
      issues.push({
        kind: "extra-in-manifest",
        id: row.id,
        detail: `§E dokumenterer «${row.id}» men rules.ts håndhever den ikke (regel fjernet uten å rydde manifestet?)`,
      });
    }
  }

  return {
    contractPath: null,
    ruleCount: rules.length,
    manifestCount: manifest.length,
    issues,
    ok: issues.length === 0,
  };
}

/** Read a contract file from disk and ratify it against RULES. */
export function ratifyFile(path: string, rules: Rule[] = RULES): RatifyResult {
  if (!existsSync(path)) {
    return {
      contractPath: path,
      ruleCount: rules.length,
      manifestCount: 0,
      issues: [
        {
          kind: "missing-from-manifest",
          id: "(contract)",
          detail: `Fant ikke kontrakten: ${path}. Sett MASKINROMMET_CONTRACT eller gi stien som argument.`,
        },
      ],
      ok: false,
    };
  }
  const text = readFileSync(path, "utf8");
  const r = ratify(text, rules);
  r.contractPath = path;
  return r;
}
