"""Deterministic claim-contradiction detection for knowledge corpora (Phase 7b)."""

from __future__ import annotations

import hashlib
import logging
import re
from itertools import combinations
from typing import Any

from utils.knowledge_themes import _cfg_bool, _cfg_float, _cfg_int, _looks_secret, _tokenize_for_themes

_log = logging.getLogger(__name__)

CONTRADICTIONS_ALGO_VERSION = "v1-heuristic"

_NEGATION_RE = re.compile(
    r"\b(not|never|no|cannot|can't|isn't|aren't|won't|without|neither|nor)\b",
    re.I,
)
_NUMBER_RE = re.compile(
    r"\b(\d+(?:\.\d+)?)\s*(%|percent|years?|days?|months?|hours?|minutes?|kg|lb|miles?|km|usd|\$)?\b",
    re.I,
)
_POLARITY_PAIRS: tuple[tuple[str, str], ...] = (
    ("increase", "decrease"),
    ("increased", "decreased"),
    ("before", "after"),
    ("required", "optional"),
    ("always", "never"),
    ("enable", "disable"),
    ("enabled", "disabled"),
    ("accept", "reject"),
    ("accepted", "rejected"),
    ("true", "false"),
    ("higher", "lower"),
    ("more", "less"),
    ("add", "remove"),
    ("added", "removed"),
)


def contradictions_enabled(cfg: dict[str, Any] | None = None) -> bool:
    """Contradictions require the entity layer (same gate as entities_enabled)."""
    from utils.knowledge_entities import entities_enabled, get_entities_config

    if not entities_enabled(get_entities_config()):
        return False
    section = cfg if cfg is not None else get_contradictions_config()
    return _cfg_bool(section, "contradictions_enabled", True)


def get_contradictions_config() -> dict[str, Any]:
    """Return [knowledge.contradictions] with safe defaults."""
    from core.config import get_knowledge_config

    sec = get_knowledge_config().get("contradictions")
    if not isinstance(sec, dict):
        sec = {}
    out = dict(sec)
    out.setdefault("contradictions_enabled", True)
    out.setdefault("max_contradictions", 20)
    out.setdefault("min_confidence", 0.0)
    out.setdefault("allow_same_source", False)
    return out


def _claim_max_chars(cfg: dict[str, Any]) -> int:
    from utils.knowledge_entities import get_entities_config

    return max(40, min(500, _cfg_int(get_entities_config(), "claim_max_chars", 200)))


def _truncate(text: str, max_chars: int) -> str:
    text = (text or "").strip()
    if len(text) <= max_chars:
        return text
    return text[: max_chars - 3].rstrip() + "..."


def _entity_label_map(entities: list[dict[str, Any]]) -> dict[str, str]:
    out: dict[str, str] = {}
    for row in entities:
        if not isinstance(row, dict):
            continue
        key = str(row.get("entity_key") or row.get("canonical_key") or "").strip().lower()
        if not key:
            continue
        label = str(row.get("label") or key).strip()
        out[key] = label or key
    return out


def _claim_entity_keys(claim: dict[str, Any]) -> list[str]:
    keys_raw = claim.get("entity_keys") or []
    if not isinstance(keys_raw, list):
        return []
    return [str(k).strip().lower() for k in keys_raw if str(k).strip()]


def _shared_entity_keys(a: dict[str, Any], b: dict[str, Any]) -> list[str]:
    ka = set(_claim_entity_keys(a))
    kb = set(_claim_entity_keys(b))
    return sorted(ka & kb)


def _content_tokens(text: str) -> set[str]:
    return set(_tokenize_for_themes((text or "").lower()))


def _negation_contradiction(text_a: str, text_b: str) -> float | None:
    low_a = (text_a or "").lower()
    low_b = (text_b or "").lower()
    neg_a = bool(_NEGATION_RE.search(low_a))
    neg_b = bool(_NEGATION_RE.search(low_b))
    if neg_a == neg_b:
        return None
    pos_text, neg_text = (low_b, low_a) if neg_a else (low_a, low_b)
    pos_tokens = _content_tokens(pos_text)
    neg_tokens = _content_tokens(neg_text)
    if not pos_tokens or not neg_tokens:
        return None
    overlap = pos_tokens & neg_tokens
    stop = {
        "the",
        "a",
        "an",
        "is",
        "are",
        "was",
        "were",
        "be",
        "been",
        "being",
        "have",
        "has",
        "had",
        "do",
        "does",
        "did",
        "will",
        "would",
        "could",
        "should",
        "may",
        "might",
        "must",
        "can",
        "not",
        "no",
        "never",
        "without",
    }
    overlap -= stop
    if len(overlap) < 2:
        return None
    return min(0.85, 0.5 + 0.05 * len(overlap))


def _numeric_contradiction(text_a: str, text_b: str) -> float | None:
    nums_a = list(_NUMBER_RE.finditer(text_a or ""))
    nums_b = list(_NUMBER_RE.finditer(text_b or ""))
    if not nums_a or not nums_b:
        return None
    for ma in nums_a:
        val_a = ma.group(1)
        unit_a = (ma.group(2) or "").lower()
        ctx_a = (text_a or "")[max(0, ma.start() - 24) : min(len(text_a), ma.end() + 24)].lower()
        ctx_tokens = set(_tokenize_for_themes(ctx_a)) - {val_a, unit_a}
        for mb in nums_b:
            val_b = mb.group(1)
            unit_b = (mb.group(2) or "").lower()
            if val_a == val_b:
                continue
            if unit_a and unit_b and unit_a != unit_b:
                continue
            ctx_b = (text_b or "")[max(0, mb.start() - 24) : min(len(text_b), mb.end() + 24)].lower()
            ctx_b_tokens = set(_tokenize_for_themes(ctx_b)) - {val_b, unit_b}
            if len(ctx_tokens & ctx_b_tokens) >= 2:
                return 0.72
    return None


def _polarity_contradiction(text_a: str, text_b: str) -> float | None:
    low_a = (text_a or "").lower()
    low_b = (text_b or "").lower()
    tokens_a = _content_tokens(low_a)
    tokens_b = _content_tokens(low_b)
    for left, right in _POLARITY_PAIRS:
        if (left in tokens_a and right in tokens_b) or (right in tokens_a and left in tokens_b):
            overlap = tokens_a & tokens_b
            if len(overlap) >= 2:
                return 0.68
    return None


def _pair_kind_and_confidence(text_a: str, text_b: str) -> tuple[str, float] | None:
    neg = _negation_contradiction(text_a, text_b)
    if neg is not None:
        return "negation", neg
    num = _numeric_contradiction(text_a, text_b)
    if num is not None:
        return "numeric", num
    pol = _polarity_contradiction(text_a, text_b)
    if pol is not None:
        return "polarity", pol
    return None


def _contradiction_id(corpus_slug: str, claim_a_id: str, claim_b_id: str) -> str:
    pair = "|".join(sorted([claim_a_id, claim_b_id]))
    return hashlib.sha256(f"{corpus_slug}:{pair}".encode()).hexdigest()[:16]


def filter_contradiction_rows_for_read(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Drop rows whose embedded excerpts look secret (defense in depth on GET/MCP)."""
    out: list[dict[str, Any]] = []
    for row in rows:
        if not isinstance(row, dict):
            continue
        block_a = row.get("claim_a")
        block_b = row.get("claim_b")
        if not isinstance(block_a, dict) or not isinstance(block_b, dict):
            continue
        text_a = str(block_a.get("text") or "").strip()
        text_b = str(block_b.get("text") or "").strip()
        if not text_a or not text_b:
            continue
        if _looks_secret(text_a) or _looks_secret(text_b):
            continue
        out.append(row)
    return out


def visible_contradiction_rows(rows: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
    """Return contradiction rows safe to surface in API/status (non-secret, well-formed)."""
    if not isinstance(rows, list):
        return []
    return filter_contradiction_rows_for_read(rows)


def contradiction_row_matches_entity(
    row: dict[str, Any],
    entity_key: str,
    *,
    claims_by_id: dict[str, dict[str, Any]] | None = None,
) -> bool:
    """True when a contradiction row involves the entity (multi-entity claims included)."""
    key = (entity_key or "").strip().lower()
    if not key:
        return False
    keys_raw = row.get("entity_keys")
    if isinstance(keys_raw, list):
        if key in [str(k).strip().lower() for k in keys_raw if str(k).strip()]:
            return True
    if str(row.get("entity_key") or "").strip().lower() == key:
        return True
    if not claims_by_id:
        return False
    for side in ("claim_a", "claim_b"):
        block = row.get(side)
        if not isinstance(block, dict):
            continue
        cid = str(block.get("id") or "").strip()
        claim = claims_by_id.get(cid)
        if claim and key in _claim_entity_keys(claim):
            return True
    return False


def detect_contradictions(
    entities: list[dict[str, Any]],
    claims: list[dict[str, Any]],
    *,
    corpus_slug: str = "",
    config: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
    """Find heuristic cross-source contradictions between claims sharing an entity."""
    cfg = config if config is not None else get_contradictions_config()
    if not contradictions_enabled(cfg):
        return []
    try:
        safe_slug = (corpus_slug or "").strip().lower()
        max_rows = max(1, min(40, _cfg_int(cfg, "max_contradictions", 20)))
        min_conf = max(0.0, min(1.0, _cfg_float(cfg, "min_confidence", 0.0)))
        allow_same = _cfg_bool(cfg, "allow_same_source", False)
        max_chars = _claim_max_chars(cfg)
        labels = _entity_label_map(entities)

        valid_claims: list[dict[str, Any]] = []
        for row in claims:
            if not isinstance(row, dict):
                continue
            text = str(row.get("claim_text") or "").strip()
            cid = str(row.get("id") or "").strip()
            if not text or not cid:
                continue
            if _looks_secret(text):
                continue
            valid_claims.append(row)

        candidates: list[dict[str, Any]] = []
        seen_pairs: set[str] = set()

        for entity_key in sorted(
            {k for c in valid_claims for k in _claim_entity_keys(c)}
        ):
            bucket = [c for c in valid_claims if entity_key in _claim_entity_keys(c)]
            if len(bucket) < 2:
                continue
            for ca, cb in combinations(bucket, 2):
                id_a = str(ca.get("id") or "")
                id_b = str(cb.get("id") or "")
                if not id_a or not id_b or id_a == id_b:
                    continue
                pair_key = "|".join(sorted([id_a, id_b]))
                if pair_key in seen_pairs:
                    continue
                slug_a = str(ca.get("source_slug") or "").strip()
                slug_b = str(cb.get("source_slug") or "").strip()
                if not allow_same:
                    if not slug_a or not slug_b:
                        continue
                    if slug_a == slug_b:
                        continue
                text_a = str(ca.get("claim_text") or "")
                text_b = str(cb.get("claim_text") or "")
                if _looks_secret(text_a) or _looks_secret(text_b):
                    continue
                scored = _pair_kind_and_confidence(text_a, text_b)
                if scored is None:
                    continue
                kind, confidence = scored
                if confidence < min_conf:
                    continue
                seen_pairs.add(pair_key)
                shared_keys = sorted(
                    set(_claim_entity_keys(ca)) & set(_claim_entity_keys(cb))
                )
                if not shared_keys:
                    continue
                row_id = _contradiction_id(safe_slug, id_a, id_b)
                if id_a > id_b:
                    ca, cb = cb, ca
                    id_a, id_b = id_b, id_a
                    text_a, text_b = text_b, text_a
                    slug_a, slug_b = slug_b, slug_a
                primary_key = shared_keys[0]
                candidates.append(
                    {
                        "id": row_id,
                        "entity_key": primary_key,
                        "entity_keys": shared_keys,
                        "entity_label": labels.get(
                            primary_key, primary_key.replace("_", " ")
                        ),
                        "kind": kind,
                        "confidence": round(confidence, 3),
                        "heuristic": True,
                        "algo_version": CONTRADICTIONS_ALGO_VERSION,
                        "claim_a": {
                            "id": id_a,
                            "text": _truncate(text_a, max_chars),
                            "source_slug": slug_a,
                        },
                        "claim_b": {
                            "id": id_b,
                            "text": _truncate(text_b, max_chars),
                            "source_slug": slug_b,
                        },
                    }
                )

        candidates.sort(key=lambda r: (-float(r.get("confidence") or 0), str(r.get("id") or "")))
        return candidates[:max_rows]
    except Exception as exc:
        _log.warning("detect_contradictions failed: %s", exc)
        return []
