#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Dimension A: Truth Verification / Contradiction Detection.

Detects 3 contradiction types: A1 factual, A2 prescriptive, A3 temporal.
Public: scan_contradictions, apply_penalties, resolve_contradiction,
        quick_contradiction_check, get_registry
"""
import hashlib
import json
import re
from datetime import datetime, timezone
from pulse.core.brain_io import _acquire
from pulse.core.brain_utils import (
    FACTS_FILE, LESSONS_JSONL, FAILS_JSONL, PATTERNS_JSONL,
    STATE_DIR, read_jsonl_entries, text_similarity, now_iso,
)

REGISTRY_FILE = STATE_DIR / "contradiction_registry.json"
_POS = re.compile(r"\b(always|must|should|need to|require|use|prefer|enable)\b", re.I)
_NEG = re.compile(r"\b(never|avoid|don'?t|must not|should not|disable|remove|stop)\b", re.I)
_EMPTY_REG = {"contradictions": [], "stats": {"total_detected": 0, "auto_resolved": 0, "pending": 0}, "last_scan": ""}


def _load_registry() -> dict:
    if not REGISTRY_FILE.exists():
        return {**_EMPTY_REG, "contradictions": []}
    try:
        d = json.loads(REGISTRY_FILE.read_text(encoding="utf-8"))
        return d if isinstance(d, dict) else {**_EMPTY_REG, "contradictions": []}
    except (json.JSONDecodeError, OSError):
        return {**_EMPTY_REG, "contradictions": []}


def _save_registry(reg: dict):
    REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
    with _acquire(REGISTRY_FILE):
        REGISTRY_FILE.write_text(json.dumps(reg, indent=2, ensure_ascii=False), encoding="utf-8")


def _ctr_id(a: str, b: str) -> str:
    pair = sorted([a[:80].lower(), b[:80].lower()])
    return "ctr_" + hashlib.md5("||".join(pair).encode()).hexdigest()[:12]


def _make_ctr(cid, ctype, file_a, text_a, conf_a, file_b, text_b, conf_b,
              sim, resolution=None, extra_a=None, extra_b=None):
    item_a = {"file": file_a, "text": text_a[:200], "confidence": conf_a}
    item_b = {"file": file_b, "text": text_b[:200], "confidence": conf_b}
    if extra_a: item_a.update(extra_a)
    if extra_b: item_b.update(extra_b)
    return {"id": cid, "type": ctype, "item_a": item_a, "item_b": item_b,
            "similarity": round(sim, 3), "detected_at": now_iso(),
            "resolution": resolution, "resolved_at": now_iso() if resolution else None,
            "resolved_by": "truth_verifier" if resolution else None}


def _is_active(item: dict) -> bool:
    return item.get("validity", item.get("temporal", "valid")) in ("valid", "contested", "current", "active")


def _store_extinction_trace(item_a_id: str, item_b_id: str, confidence: float = 0.5):
    """F41: Store a CONTRADICTS edge in the graph — creates an extinction trace.

    The brain remembers that these two items conflict, so future retrievals
    can be weighted accordingly.
    """
    try:
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        conn.execute(
            "INSERT OR IGNORE INTO graph_edges (from_node, to_node, edge_type, confidence) "
            "VALUES (?, ?, 'CONTRADICTS', ?)",
            (item_a_id, item_b_id, confidence)
        )
        conn.commit()
    except Exception:
        pass  # fail-open: extinction trace is non-critical


def _load_facts() -> list[dict]:
    if not FACTS_FILE.exists():
        return []
    try:
        d = json.loads(FACTS_FILE.read_text(encoding="utf-8"))
        return d if isinstance(d, list) else []
    except (json.JSONDecodeError, OSError):
        return []


def _load_knowledge() -> list[dict]:
    items = []
    for path, tag in [(LESSONS_JSONL, "lesson"), (FAILS_JSONL, "failure"), (PATTERNS_JSONL, "pattern")]:
        for e in read_jsonl_entries(path):
            e.setdefault("_src", tag)
            items.append(e)
    return items


def _scan_factual(facts, seen):
    from pulse.brain.persistence.fact_store import _entity_key
    groups: dict[str, list] = {}
    for f in facts:
        if not _is_active(f):
            continue
        ek = _entity_key(f)
        if ek and ek != ":":
            groups.setdefault(ek, []).append(f)
    out = []
    for group in groups.values():
        for i in range(len(group)):
            for j in range(i + 1, len(group)):
                a, b = group[i], group[j]
                sim = text_similarity(a.get("fact", "")[:80], b.get("fact", "")[:80])
                if sim < 0.4:
                    cid = _ctr_id(a.get("fact", ""), b.get("fact", ""))
                    if cid not in seen:
                        out.append(_make_ctr(cid, "factual", "auto_facts.json",
                                   a.get("fact", ""), a.get("confidence", 0.5),
                                   "auto_facts.json", b.get("fact", ""), b.get("confidence", 0.5), sim))
    return out


def _extract_polarity(text: str):
    tl = text.lower()
    m = _NEG.search(tl)
    if m:
        return "negative", tl[m.end():].strip()[:60]
    m = _POS.search(tl)
    if m:
        return "positive", tl[m.end():].strip()[:60]
    return None, ""


def _scan_prescriptive(items, seen):
    pol = []
    for it in items:
        if not _is_active(it):
            continue
        txt = it.get("content", "") or it.get("title", "")
        p, obj = _extract_polarity(txt)
        if p and len(obj) > 5:
            pol.append((p, obj, it))
    out = []
    for i in range(len(pol)):
        for j in range(i + 1, len(pol)):
            if pol[i][0] == pol[j][0]:
                continue
            obj_sim = text_similarity(pol[i][1], pol[j][1])
            if obj_sim >= 0.5:
                ta = pol[i][2].get("content", "") or pol[i][2].get("title", "")
                tb = pol[j][2].get("content", "") or pol[j][2].get("title", "")
                cid = _ctr_id(ta, tb)
                if cid not in seen:
                    out.append(_make_ctr(cid, "prescriptive",
                               pol[i][2].get("_src", "?"), ta, pol[i][2].get("confidence", 0.5),
                               pol[j][2].get("_src", "?"), tb, pol[j][2].get("confidence", 0.5),
                               obj_sim,
                               extra_a={"consolidation_count": pol[i][2].get("consolidation_count", 1)},
                               extra_b={"consolidation_count": pol[j][2].get("consolidation_count", 1)}))
    return out


def _scan_temporal(facts, seen):
    from pulse.brain.persistence.fact_store import _entity_key
    from pulse.core.temporal import validity_transition
    groups: dict[str, list] = {}
    for f in facts:
        ek = _entity_key(f)
        if ek and ek != ":":
            groups.setdefault(ek, []).append(f)
    out = []
    for group in groups.values():
        if len(group) < 2:
            continue
        group.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
        newest = group[0]
        for old in group[1:]:
            v = old.get("validity", old.get("temporal", "valid"))
            if v in ("valid", "current"):
                cid = _ctr_id(newest.get("fact", ""), old.get("fact", ""))
                if cid not in seen:
                    out.append(_make_ctr(cid, "temporal", "auto_facts.json",
                               newest.get("fact", ""), newest.get("confidence", 0.5),
                               "auto_facts.json", old.get("fact", ""), old.get("confidence", 0.5),
                               0.0, resolution="auto_temporal"))
                    ns = validity_transition(old.get("validity", "valid"), "superseded")
                    if ns:
                        old["validity"] = ns
    return out


# === PUBLIC API ===

def scan_contradictions(verbose=False) -> dict:
    """Run all 3 contradiction scanners and store results."""
    reg = _load_registry()
    seen = {c["id"] for c in reg.get("contradictions", [])}
    facts = _load_facts()
    new = _scan_factual(facts, seen) + _scan_prescriptive(_load_knowledge(), seen) + _scan_temporal(facts, seen)
    if new:
        reg["contradictions"].extend(new)
        # F41: Extinction trace — store CONTRADICTS edges for all new contradictions
        for ctr in new:
            a_id = ctr.get("item_a", {}).get("id") or ctr["item_a"].get("text", "")[:40]
            b_id = ctr.get("item_b", {}).get("id") or ctr["item_b"].get("text", "")[:40]
            avg_conf = (ctr["item_a"].get("confidence", 0.5) + ctr["item_b"].get("confidence", 0.5)) / 2
            _store_extinction_trace(a_id, b_id, avg_conf)
    reg["last_scan"] = now_iso()
    pending = sum(1 for c in reg["contradictions"] if not c.get("resolution"))
    reg["stats"] = {"total_detected": len(reg["contradictions"]),
                    "auto_resolved": len(reg["contradictions"]) - pending, "pending": pending}
    _save_registry(reg)
    by_type = {t: sum(1 for c in new if c["type"] == t) for t in ("factual", "prescriptive", "temporal")}
    if verbose:
        print(f"  [TRUTH] {len(new)} new (F={by_type['factual']}, P={by_type['prescriptive']}, T={by_type['temporal']})")
    return {"new": len(new), "total": len(reg["contradictions"]), "pending": pending, "by_type": by_type}


def apply_penalties(verbose=False) -> int:
    """Lower confidence on contradicted items via Bayesian negative update."""
    from pulse.core.uncertainty import bayesian_update
    reg = _load_registry()
    penalized = 0
    now = datetime.now(timezone.utc)
    for ctr in reg.get("contradictions", []):
        if ctr.get("resolution"):
            continue
        # Weekly extra decay for unresolved > 7 days
        try:
            dt = datetime.fromisoformat(ctr.get("detected_at", "").replace("Z", "+00:00"))
            weeks = (now - dt).days / 7
            if weeks > 1:
                extra = min(0.2, 0.05 * int(weeks))
                for s in ("item_a", "item_b"):
                    ctr[s]["confidence"] = max(0.05, round(ctr[s].get("confidence", 0.5) - extra, 4))
        except (ValueError, TypeError):
            pass
        if ctr["type"] == "factual":
            for s in ("item_a", "item_b"):
                c, _ = bayesian_update(ctr[s].get("confidence", 0.5), ctr[s].get("evidence_count", 1), False)
                ctr[s]["confidence"] = c
                penalized += 1
        elif ctr["type"] == "prescriptive":
            loser = "item_b" if ctr["item_a"].get("consolidation_count", 1) >= ctr["item_b"].get("consolidation_count", 1) else "item_a"
            c, _ = bayesian_update(ctr[loser].get("confidence", 0.5), 1, False)
            c, _ = bayesian_update(c, 2, False)
            ctr[loser]["confidence"] = c
            penalized += 1
    _save_registry(reg)
    if verbose and penalized:
        print(f"  [TRUTH] Penalized {penalized} items")
    return penalized


def resolve_contradiction(ctr_id: str, resolution: str, resolved_by: str = "consolidation") -> bool:
    """Mark a contradiction as resolved."""
    reg = _load_registry()
    for c in reg.get("contradictions", []):
        if c["id"] == ctr_id:
            c["resolution"] = resolution
            c["resolved_at"] = now_iso()
            c["resolved_by"] = resolved_by
            _save_registry(reg)
            return True
    return False


def quick_contradiction_check(description: str, knowledge_type: str = "") -> bool:
    """Fast routing-time check — does this text match a pending contradiction?"""
    reg = _load_registry()
    dl = description[:80].lower()
    for c in reg.get("contradictions", []):
        if c.get("resolution"):
            continue
        for s in ("item_a", "item_b"):
            if text_similarity(dl, c[s].get("text", "")[:80].lower()) >= 0.5:
                return True
    return False


def get_registry() -> dict:
    """Load and return the contradiction registry."""
    return _load_registry()


def _compute_calibration() -> dict:
    """Item 78: Confidence calibration — predicted vs actual success rate."""
    try:
        from pulse.core.brain_db import get_conn
        row = get_conn().execute(
            "SELECT COUNT(*) as total, SUM(CASE WHEN validity!='expired' THEN 1 ELSE 0 END) as valid "
            "FROM lessons WHERE confidence>=0.8").fetchone()
        total = (row[0] if isinstance(row, tuple) else row["total"]) or 1
        valid = (row[1] if isinstance(row, tuple) else row["valid"]) or 0
        return {"calibration_ratio": round(valid / total, 3), "total_high_conf": total, "valid_count": valid}
    except Exception:
        return {"calibration_ratio": 1.0, "total_high_conf": 0, "valid_count": 0}
