#!/usr/bin/env python3
"""facts/audit - evidence & lexicon checks for the 2dph brain.

  bin/facts/audit self         # lexicon: docs + two-source rule
  bin/facts/audit db           # evidence gate against var/kb.lbug
  bin/facts/audit contradict   # D16 adjudication (JSON claim(s) on stdin)

`self` mode checks the repo itself (no network, no runtime deps).
`db` mode loads every Leaf with root=facts. Confirmed facts need ` x `;
hypothesis contradictions need `a x b vs c x d` (both sides ≥2).
`contradict` applies temporal_freshness then authority_pairing; ≥2 vs ≥2
with no rule stays hypothesis / `(not confirmed)`.

Exit 0 = all checks pass, 1 = audit failures, 2 = could not evaluate.
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "bin" / "tools"))

from contradict import adjudicate, check_fact_row  # noqa: E402


def audit_db() -> list[str]:
    from kblib import connect
    from kblib import VAR
    dbpath = VAR / "kb.lbug"
    if not dbpath.exists():
        return ["no database yet; run bin/kb/index first"]
    db, conn = connect(dbpath)
    r = conn.execute("MATCH (l:Leaf {root:'facts'}) RETURN l.id, l.source, l.loc, l.how, l.confidence")
    problems: list[str] = []
    for lid, source, loc, how, conf in r.get_all():
        problems.extend(check_fact_row(str(lid), str(source or ""), str(loc or ""),
                                       str(how or ""), str(conf or "")))
    conn.close()
    db.close()
    return problems


def audit_self() -> list[str]:
    problems: list[str] = []
    plan = (ROOT / "PLAN.md").read_text()

    if "recall@5" not in plan:
        problems.append("PLAN.md missing recall@5 gate")
    if re.search(r"(?i)facts must have.*2 sources|2.source", plan) is None:
        problems.append("PLAN.md missing the two-source evidence rule for facts")
    if "temporal_freshness" not in plan or "authority_pairing" not in plan:
        problems.append("PLAN.md missing D16 adjudication rules")
    if re.search(r"(?i)HNSW|BM25|deduction", (ROOT / "README.md").read_text()) is None:
        problems.append("README.md missing search/retrieval description")
    return problems


def audit_contradict(raw: str) -> tuple[list[str], list[dict]]:
    raw = raw.strip()
    if not raw:
        return ["contradict: empty stdin (JSON claim or {claims:[...]})"], []
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as e:
        return [f"contradict: invalid JSON: {e}"], []
    if isinstance(payload, dict) and "claims" in payload:
        claims = list(payload.get("claims") or [])
    elif isinstance(payload, dict):
        claims = [payload]
    elif isinstance(payload, list):
        claims = payload
    else:
        return ["contradict: expected object or list"], []
    details = [adjudicate(c) for c in claims]
    return [], details


def main(argv: list[str]) -> int:
    import argparse
    p = argparse.ArgumentParser(description="evidence & lexicon audit")
    p.add_argument("mode", choices=("self", "db", "contradict"))
    p.add_argument("--json", action="store_true")
    a = p.parse_args(argv)

    details: list[dict] = []
    if a.mode == "self":
        problems = audit_self()
    elif a.mode == "db":
        problems = audit_db()
    else:
        problems, details = audit_contradict(sys.stdin.read())
    out: dict = {"mode": a.mode, "ok": not problems, "problems": problems}
    if details:
        out["contradictions"] = details
    if a.json:
        print(json.dumps(out, indent=2))
    else:
        from yamlout import to_yaml
        print(to_yaml(out))
    return 0 if not problems else 1


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
