"""
bayes_analiz.verification — Verification engine for the Bayes Lens (Lens #4 of 7).

Provides checks and a convergence score (yakinlasma) for Bayesian models.
Each check returns ``(passed: bool, detail: str)`` for integration with
the framework quality apparatus (AGENTS.md §5).

Checks implemented:
  - prior_normalisation:        Prior probabilities in valid range
  - likelihood_validity:        Likelihoods in valid range
  - evidence_independence:      KV₇ — sources must be independent
  - posterior_consistency:       Posterior matches Bayesian update chain
  - convergence_bound_check:    KV₄/T6 — no score ≥ 1.0
  - tesanud_infirad_check:      AX63 — composition asymmetry
  - hypothesis_coverage:        Minimum hypothesis coverage
  - transparency_check:         AX57 — model must be inspectable

Governing axioms:
  AX21:  Continuous degrees
  AX22:  Unreachable supremum
  AX63:  Tesanüd/İnfirâd asymmetry
  KV₃:   Observer non-interference (methodology detects, never creates)
  KV₄/T6: Convergence bound 0 < C < 1
  KV₇:   Independence

KV₇ compliance: This module imports ONLY from bayes_analiz.types
                 and the standard library.
"""

from __future__ import annotations

from typing import Dict, List, Optional, Tuple

from bayes_analiz.types import (
    BayesModel,
    CompositionMode,
    Evidence,
    EvidenceType,
    Hypothesis,
    HypothesisStatus,
    PosteriorEntry,
    clamp_score,
)


# ---------------------------------------------------------------------------
# Individual verification checks
# ---------------------------------------------------------------------------

def check_prior_normalisation(model: BayesModel) -> Tuple[bool, str]:
    """Verify all hypothesis priors are in (0, 1).

    Per AX21 (continuous degrees) and AX22 (unreachable supremum),
    priors must be strictly between 0 and 1.
    """
    if not model.hypotheses:
        return False, "No hypotheses registered"
    for name, h in model.hypotheses.items():
        if not (0.0 < h.prior < 1.0):
            return False, f"Hypothesis '{name}' has invalid prior {h.prior}"
    return True, f"All {len(model.hypotheses)} priors valid"


def check_likelihood_validity(model: BayesModel) -> Tuple[bool, str]:
    """Verify all evidence likelihoods are in (0, 1).

    Both P(E|H) and P(E|¬H) must be strictly between 0 and 1.
    """
    if not model.evidence_items:
        return False, "No evidence items registered"
    for name, e in model.evidence_items.items():
        if not (0.0 < e.likelihood < 1.0):
            return False, (
                f"Evidence '{name}' has invalid likelihood {e.likelihood}"
            )
        if not (0.0 < e.likelihood_complement < 1.0):
            return False, (
                f"Evidence '{name}' has invalid likelihood_complement "
                f"{e.likelihood_complement}"
            )
    return True, f"All {len(model.evidence_items)} evidence likelihoods valid"


def check_evidence_independence(model: BayesModel) -> Tuple[bool, str]:
    """Verify evidence source independence — KV₇.

    Per KV₇ (İhlâs / independence): meaningful convergence requires
    independent sources.  Flag any non-independent sources.
    """
    if not model.evidence_items:
        return True, "No evidence to check"
    non_independent = [
        name for name, e in model.evidence_items.items()
        if not e.source_independent
    ]
    if non_independent:
        return False, (
            f"Non-independent evidence sources: {non_independent}. "
            "KV₇: convergence requires independence."
        )
    return True, f"All {len(model.evidence_items)} evidence sources independent"


def check_posterior_consistency(model: BayesModel) -> Tuple[bool, str]:
    """Verify the update trail is consistent with Bayes' theorem.

    Re-computes each posterior from the recorded prior and evidence,
    checks that updated posteriors match stored values (within tolerance).
    """
    if not model.updates:
        return True, "No updates to verify"
    tolerance = 1e-6
    for i, entry in enumerate(model.updates):
        if entry.evidence_name not in model.evidence_items:
            return False, (
                f"Update #{i}: evidence '{entry.evidence_name}' not in model"
            )
        ev = model.evidence_items[entry.evidence_name]
        # Recompute
        numerator = ev.likelihood * entry.prior
        denominator = (ev.likelihood * entry.prior +
                       ev.likelihood_complement * (1.0 - entry.prior))
        if denominator == 0.0:
            expected = entry.prior
        else:
            expected = numerator / denominator
        expected = min(max(expected, 0.0001), 0.9999)
        if abs(entry.posterior - expected) > tolerance:
            return False, (
                f"Update #{i}: recorded posterior {entry.posterior:.6f} "
                f"≠ computed {expected:.6f}"
            )
    return True, f"All {len(model.updates)} updates consistent"


def check_convergence_bound(model: BayesModel) -> Tuple[bool, str]:
    """Verify all posteriors respect KV₄/T6: 0 < posterior < 1.

    If any posterior ≥ 0.95, flag per KV₄ warning threshold.
    """
    warnings: List[str] = []
    for name, posterior in model.current_posteriors.items():
        if posterior <= 0.0 or posterior >= 1.0:
            return False, (
                f"Hypothesis '{name}' posterior {posterior} violates "
                "convergence bound (must be in (0, 1))"
            )
        if posterior >= 0.95:
            warnings.append(
                f"'{name}' posterior {posterior:.4f} ≥ 0.95"
            )
    if warnings:
        return False, (
            f"KV₄ warning — convergence approaching 1.0: "
            f"{'; '.join(warnings)}"
        )
    return True, "All posteriors within convergence bound"


def check_tesanud_infirad(model: BayesModel) -> Tuple[bool, str]:
    """Verify the tesanüd/infirâd asymmetry — AX63.

    Checks that:
    1. Confirming evidence from independent sources is tagged TESANUD
    2. Disconfirming evidence is tagged INFIRAD (does not compound)
    """
    if not model.updates:
        return True, "No updates to verify"
    violations: List[str] = []
    for entry in model.updates:
        if entry.evidence_name not in model.evidence_items:
            continue
        ev = model.evidence_items[entry.evidence_name]
        bf = entry.bayes_factor
        if bf > 1.0 and ev.source_independent:
            if entry.composition_mode != CompositionMode.TESANUD:
                violations.append(
                    f"'{entry.evidence_name}' → '{entry.hypothesis_name}': "
                    f"BF > 1 + independent but mode={entry.composition_mode.value}"
                )
        elif bf < 1.0:
            if entry.composition_mode != CompositionMode.INFIRAD:
                violations.append(
                    f"'{entry.evidence_name}' → '{entry.hypothesis_name}': "
                    f"BF < 1 but mode={entry.composition_mode.value}"
                )
    if violations:
        return False, (
            f"AX63 violations: {'; '.join(violations)}"
        )
    return True, "Tesanüd/İnfirâd labels consistent with AX63"


def check_hypothesis_coverage(
    model: BayesModel,
    min_hypotheses: int = 1,
) -> Tuple[bool, str]:
    """Verify the model has sufficient hypothesis coverage.

    Per AX52 (multiplicative gate): if ANY dimension has zero coverage,
    the entire gate collapses.
    """
    count = len(model.hypotheses)
    if count < min_hypotheses:
        return False, (
            f"Hypothesis coverage {count} < minimum {min_hypotheses}. "
            "AX52: zero coverage → gate collapse."
        )
    # Check that every hypothesis has at least one update (evidence coverage)
    uncovered = [
        name for name in model.hypotheses
        if not model.get_updates_for(name)
    ]
    evidence_msg = ""
    if uncovered:
        evidence_msg = f" ({len(uncovered)} hypotheses without evidence: {uncovered})"
    return True, f"{count} hypotheses registered{evidence_msg}"


def check_transparency(model: BayesModel) -> Tuple[bool, str]:
    """Verify the model satisfies AX57 (transparency requirement).

    The model must be fully inspectable: to_dict() must succeed and
    contain the required keys.
    """
    try:
        d = model.to_dict()
    except Exception as exc:
        return False, f"AX57 violated — model not serialisable: {exc}"
    required_keys = {"name", "hypotheses", "evidence_items", "update_count"}
    missing = required_keys - set(d.keys())
    if missing:
        return False, f"AX57 violated — missing keys in model dict: {missing}"
    return True, "Model is fully transparent (AX57)"


# ---------------------------------------------------------------------------
# Aggregated verification
# ---------------------------------------------------------------------------

def verify_all(model: BayesModel) -> Dict[str, Tuple[bool, str]]:
    """Run all verification checks on a BayesModel.

    Returns:
        Dict mapping check name → (passed, detail_message).
    """
    return {
        "prior_normalisation": check_prior_normalisation(model),
        "likelihood_validity": check_likelihood_validity(model),
        "evidence_independence": check_evidence_independence(model),
        "posterior_consistency": check_posterior_consistency(model),
        "convergence_bound": check_convergence_bound(model),
        "tesanud_infirad": check_tesanud_infirad(model),
        "hypothesis_coverage": check_hypothesis_coverage(model),
        "transparency": check_transparency(model),
    }


# ---------------------------------------------------------------------------
# Convergence score — yakinlasma
# ---------------------------------------------------------------------------

def yakinlasma(model: Optional[BayesModel] = None) -> float:
    """Compute the convergence score for the Bayes lens applied to a model.

    Per AGENTS.md Appendix C.4:
      yakinlasma: S_Mercek × S_Text → [0, 1)

    Weighted composite of check results:
      - Prior normalisation:     weight 0.10
      - Likelihood validity:     weight 0.10
      - Evidence independence:   weight 0.15  (KV₇)
      - Posterior consistency:   weight 0.20
      - Convergence bound:       weight 0.15  (KV₄/T6)
      - Tesanüd/İnfirâd:        weight 0.15  (AX63)
      - Hypothesis coverage:     weight 0.05
      - Transparency:            weight 0.10  (AX57)

    Returns a score in [0, 1) per T6/KV₄.
    """
    if model is None:
        return 0.0

    weights = {
        "prior_normalisation": 0.10,
        "likelihood_validity": 0.10,
        "evidence_independence": 0.15,
        "posterior_consistency": 0.20,
        "convergence_bound": 0.15,
        "tesanud_infirad": 0.15,
        "hypothesis_coverage": 0.05,
        "transparency": 0.10,
    }

    results = verify_all(model)
    score = sum(
        weights[k] * (1.0 if passed else 0.0)
        for k, (passed, _) in results.items()
    )
    return clamp_score(score)


# ---------------------------------------------------------------------------
# Framework summary
# ---------------------------------------------------------------------------

def framework_summary(model: Optional[BayesModel] = None) -> dict:
    """Produce a framework-style summary for AX57 disclosure.

    Returns a dict with:
      - lens: "Bayes"
      - faculty: "Kalb"
      - convergence_score: yakinlasma output
      - checks: {name: {passed, detail}}
      - hypothesis_count, evidence_count, update_count
      - tesanud_counts: {hypothesis_name: count of independent confirmations}
    """
    if model is None:
        return {
            "lens": "Bayes",
            "faculty": "Kalb",
            "convergence_score": 0.0,
            "checks": {},
            "hypothesis_count": 0,
            "evidence_count": 0,
            "update_count": 0,
            "tesanud_counts": {},
        }

    results = verify_all(model)
    tesanud = {
        name: model.count_independent_confirmations(name)
        for name in model.hypotheses
    }

    return {
        "lens": "Bayes",
        "faculty": "Kalb",
        "convergence_score": yakinlasma(model),
        "checks": {
            k: {"passed": v[0], "detail": v[1]}
            for k, v in results.items()
        },
        "hypothesis_count": len(model.hypotheses),
        "evidence_count": len(model.evidence_items),
        "update_count": len(model.updates),
        "tesanud_counts": tesanud,
    }
