/**
 * Per-edition lived-specifics BINDING (Fix #2, slice 2 — kilde-så-draft).
 *
 * The bank (`bank.ts`) is the operator's global inventory of real material. This
 * module is the per-EDITION bridge: a slot-map that binds each load-bearing
 * key-point (or, later, section) of one edition to either a real specific from
 * the bank OR an explicit escape decision. It lives in the edition's
 * `edition-state.json` under `articles.NN.livedSpecifics`; this module only
 * validates and reports on it (pure, testable). The elicitation that POPULATES
 * the slot-map — the guided interview that refuses vague answers — lives in the
 * command layer (`/linkedin:newsletter` Step 1.5, slice 3).
 *
 * THE GATE (retning §3 + drømme-spec G4, «vaghet avvises»): a load-bearing claim
 * must be grounded in real material (`specific`) or explicitly escape-hatched
 * (`abstrakt` with a justification, or `ekstern` = backed by research, not lived).
 * A slot left `unresolved` is the vagueness the gate rejects → BLOCK. Numbers
 * stay guilty-until-checked: a slot backed by an UNVERIFIED number is allowed but
 * WARNED (regel 6/7), so Step 2 research verifies it before a draft asserts it.
 */

import type { Bank, Specific } from "./types.js";

/** A slot is a load-bearing key-point (Step 1.5) or a section (Step 2.5 refinement). */
export type SlotKind = "key-point" | "section";

/** How a slot is grounded. `unresolved` is the vagueness the gate blocks on. */
export type Binding =
  | { type: "specific"; specificId: string } // grounded in real bank material (best)
  | { type: "abstrakt"; rationale: string } // deliberately abstract here — must justify
  | { type: "ekstern"; source?: string } // backed by external research, not lived material
  | { type: "unresolved" }; // not yet decided → BLOCK

export interface Slot {
  slotId: string;
  kind: SlotKind;
  /** Human-readable claim/key-point text. */
  label: string;
  binding: Binding;
}

export type LivedSpecificsStatus = "pending" | "bound";

export interface LivedSpecifics {
  slots: Slot[];
  status: LivedSpecificsStatus;
}

export type BindingVerdict = "PASS" | "BLOCK";

export type SlotProblemReason = "unresolved" | "dangling-specific" | "unjustified-abstrakt";

export interface SlotProblem {
  slotId: string;
  reason: SlotProblemReason;
  detail: string;
}

export interface SlotWarning {
  slotId: string;
  reason: "unverified-number";
  detail: string;
}

/** Coverage counts by binding TYPE (a dangling specific still counts under `specific`). */
export interface BindingCoverage {
  total: number;
  specific: number;
  abstrakt: number;
  ekstern: number;
  unresolved: number;
}

export interface BindingResult {
  verdict: BindingVerdict;
  /** BLOCK reasons — every offending slot, not short-circuited. */
  problems: SlotProblem[];
  /** Non-blocking direction (regel 6/7 number-verification). */
  warnings: SlotWarning[];
  coverage: BindingCoverage;
}

const isBlank = (s: string): boolean => s.trim().length === 0;

/**
 * Validate one edition's slot-map against the bank. Pure: no I/O, no clock.
 * BLOCK iff any slot is `unresolved`, points at a `specificId` absent from the
 * bank, or is an `abstrakt` escape with a blank rationale. Otherwise PASS.
 * Warnings (do not block) flag slots backed by an unverified number (regel 6/7).
 */
export function validateBinding(ls: LivedSpecifics, bank: Bank): BindingResult {
  const byId = new Map<string, Specific>(bank.specifics.map((s) => [s.id, s]));
  const problems: SlotProblem[] = [];
  const warnings: SlotWarning[] = [];
  const coverage: BindingCoverage = {
    total: ls.slots.length,
    specific: 0,
    abstrakt: 0,
    ekstern: 0,
    unresolved: 0,
  };

  for (const slot of ls.slots) {
    const b = slot.binding;
    switch (b.type) {
      case "specific": {
        coverage.specific++;
        const spec = byId.get(b.specificId);
        if (!spec) {
          problems.push({
            slotId: slot.slotId,
            reason: "dangling-specific",
            detail: `specificId '${b.specificId}' is not in the bank`,
          });
        } else if (spec.type === "number" && spec.verification === "unverified") {
          warnings.push({
            slotId: slot.slotId,
            reason: "unverified-number",
            detail: `number '${b.specificId}' is unverified — verify before the draft asserts it (regel 6/7)`,
          });
        }
        break;
      }
      case "abstrakt": {
        coverage.abstrakt++;
        if (isBlank(b.rationale)) {
          problems.push({
            slotId: slot.slotId,
            reason: "unjustified-abstrakt",
            detail: "an abstrakt escape needs a rationale — an unjustified abstraction is vaghet",
          });
        }
        break;
      }
      case "ekstern": {
        coverage.ekstern++; // source optional — Step 2 research fills it
        break;
      }
      case "unresolved": {
        coverage.unresolved++;
        problems.push({
          slotId: slot.slotId,
          reason: "unresolved",
          detail: "slot is unresolved — bind it to a specific, or escape-hatch as abstrakt/ekstern",
        });
        break;
      }
    }
  }

  return {
    verdict: problems.length === 0 ? "PASS" : "BLOCK",
    problems,
    warnings,
    coverage,
  };
}

/**
 * The bank ids an edition's slot-map actually consumed — slot order, deduped.
 * Pure. This is the input to `recordUsage` at lock: only `specific` bindings
 * count as consumption (abstrakt/ekstern/unresolved consume no bank material).
 */
export function boundSpecificIds(ls: LivedSpecifics): string[] {
  const seen = new Set<string>();
  const ids: string[] = [];
  for (const slot of ls.slots) {
    if (slot.binding.type !== "specific") continue;
    const { specificId } = slot.binding;
    if (seen.has(specificId)) continue;
    seen.add(specificId);
    ids.push(specificId);
  }
  return ids;
}
