"""
bayes_analiz.constraints — ai_assert constraint factories for the Bayes Lens.

Each factory returns an ``ai_assert.Constraint`` instance whose
``check_fn`` accepts a JSON string and returns ``(passed, score, message)``.

Constraints enforce:
  KV₄/T6:  Convergence bound (0 < C < 1)
  KV₇:     Independence — evidence sources must be independent
  AX21:    Continuous degrees — no binary priors/posteriors
  AX22:    Unreachable supremum — no probability reaches 0 or 1
  AX57:    Transparency — model state must be inspectable
  AX63:    Tesanüd/İnfirâd asymmetry — composition correctness

KV₇ compliance: This module imports ONLY from ai_assert and
                 bayes_analiz.types / bayes_analiz.verification,
                 plus the standard library.
"""

from __future__ import annotations

import json
from typing import Tuple

from ai_assert import Constraint

from bayes_analiz.types import (
    BayesModel,
    Evidence,
    EvidenceType,
    Hypothesis,
    clamp_score,
)
from bayes_analiz.verification import (
    check_convergence_bound,
    check_evidence_independence,
    check_likelihood_validity,
    check_posterior_consistency,
    check_prior_normalisation,
    check_tesanud_infirad,
    check_transparency,
    yakinlasma,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _parse_model(output: str) -> Tuple[bool, BayesModel | None, str]:
    """Parse a JSON string into a BayesModel.

    Expected schema::

        {
          "name": "...",
          "hypotheses": [
            {"name": "...", "prior": 0.5, "description": "..."},
            ...
          ],
          "evidence": [
            {
              "name": "...",
              "likelihood": 0.8,
              "likelihood_complement": 0.2,
              "type": "confirming",
              "independent": true,
              "description": "..."
            },
            ...
          ],
          "updates": [
            {"hypothesis": "...", "evidence": "..."},
            ...
          ]
        }

    Returns:
        (success, model_or_None, message)
    """
    try:
        data = json.loads(output)
    except (json.JSONDecodeError, TypeError) as exc:
        return False, None, f"BayesModel: output is not valid JSON — {exc}"

    if not isinstance(data, dict):
        return False, None, "BayesModel: JSON root must be an object"

    name = data.get("name", "unnamed")
    model = BayesModel(name=str(name))

    # Parse hypotheses
    for h_data in data.get("hypotheses", []):
        if not isinstance(h_data, dict):
            continue
        h_name = h_data.get("name", "")
        if not h_name:
            continue
        try:
            h = Hypothesis(
                name=str(h_name),
                description=str(h_data.get("description", "")),
                prior=float(h_data.get("prior", 0.5)),
            )
            model.add_hypothesis(h)
        except (ValueError, TypeError) as exc:
            return False, None, f"BayesModel: invalid hypothesis '{h_name}' — {exc}"

    # Parse evidence
    type_map = {v.value: v for v in EvidenceType}
    for e_data in data.get("evidence", []):
        if not isinstance(e_data, dict):
            continue
        e_name = e_data.get("name", "")
        if not e_name:
            continue
        try:
            e = Evidence(
                name=str(e_name),
                likelihood=float(e_data.get("likelihood", 0.5)),
                likelihood_complement=float(
                    e_data.get("likelihood_complement", 0.5)
                ),
                evidence_type=type_map.get(
                    e_data.get("type", "neutral"), EvidenceType.NEUTRAL
                ),
                description=str(e_data.get("description", "")),
                source_independent=bool(e_data.get("independent", True)),
            )
            model.add_evidence(e)
        except (ValueError, TypeError) as exc:
            return False, None, f"BayesModel: invalid evidence '{e_name}' — {exc}"

    # Execute updates
    for u_data in data.get("updates", []):
        if not isinstance(u_data, dict):
            continue
        h_name = u_data.get("hypothesis", "")
        e_name = u_data.get("evidence", "")
        if h_name and e_name:
            try:
                model.update(str(h_name), str(e_name))
            except KeyError as exc:
                return False, None, f"BayesModel: update error — {exc}"

    return True, model, "BayesModel parsed successfully"


# ---------------------------------------------------------------------------
# Constraint factories
# ---------------------------------------------------------------------------

def valid_priors() -> Constraint:
    """Constraint: all hypothesis priors must be in (0, 1).

    Enforces AX21 (continuous degrees) and AX22 (unreachable supremum).
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_prior_normalisation(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="valid_priors", check_fn=check)


def valid_likelihoods() -> Constraint:
    """Constraint: all evidence likelihoods must be in (0, 1).

    Enforces AX22 (unreachable supremum).
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_likelihood_validity(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="valid_likelihoods", check_fn=check)


def evidence_independence() -> Constraint:
    """Constraint: all evidence sources must be independent — KV₇.

    Per KV₇ (İhlâs): meaningful convergence requires independence
    between information channels.
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_evidence_independence(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="evidence_independence", check_fn=check)


def posterior_consistency() -> Constraint:
    """Constraint: all posterior updates must be consistent with Bayes' theorem.

    Re-derives each posterior from its prior and evidence likelihoods.
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_posterior_consistency(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="posterior_consistency", check_fn=check)


def convergence_bound() -> Constraint:
    """Constraint: KV₄/T6 — all posteriors in (0, 1), none ≥ 0.95.

    If any posterior ≥ 0.95, flags the KV₄ warning: convergence
    approaching 1.0 indicates error, not success.
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_convergence_bound(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="convergence_bound", check_fn=check)


def tesanud_infirad() -> Constraint:
    """Constraint: AX63 — composition asymmetry must be correctly labelled.

    Confirming + independent evidence → TESANUD (super-additive).
    Disconfirming evidence → INFIRAD (isolated).
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_tesanud_infirad(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="tesanud_infirad", check_fn=check)


def model_transparency() -> Constraint:
    """Constraint: AX57 — model must be fully transparent / inspectable."""
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        passed, detail = check_transparency(model)
        return passed, clamp_score(0.99 if passed else 0.0), detail

    return Constraint(name="model_transparency", check_fn=check)


def bayes_convergence_score() -> Constraint:
    """Constraint: the Bayes lens convergence score must be > 0 and < 1.

    Uses the yakinlasma function to compute overall Bayes lens quality,
    then checks that the result satisfies T6 (strict bound < 1.0).

    Note: The KV₄ ≥ 0.95 warning applies to the bileshke (cross-lens
    composite), not to individual lens quality metrics.
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg
        score = yakinlasma(model)
        if score >= 1.0:
            return False, clamp_score(score), (
                f"T6 violation: Bayes yakinlasma = {score:.4f} ≥ 1.0"
            )
        return True, clamp_score(score), (
            f"Bayes yakinlasma = {score:.4f}"
        )

    return Constraint(name="bayes_convergence_score", check_fn=check)


def valid_bayes_entry() -> Constraint:
    """Composite constraint: validates a complete Bayes model entry.

    Combines all individual checks into a single pass/fail gate.
    Per AX52 (multiplicative gate): if ANY dimension fails, the whole fails.
    """
    def check(output: str) -> Tuple[bool, float, str]:
        ok, model, msg = _parse_model(output)
        if not ok or model is None:
            return False, 0.0, msg

        checks = {
            "priors": check_prior_normalisation(model),
            "likelihoods": check_likelihood_validity(model),
            "independence": check_evidence_independence(model),
            "posteriors": check_posterior_consistency(model),
            "convergence": check_convergence_bound(model),
            "tesanud": check_tesanud_infirad(model),
            "transparency": check_transparency(model),
        }

        failed = {k: v[1] for k, v in checks.items() if not v[0]}
        if failed:
            details = "; ".join(f"{k}: {v}" for k, v in failed.items())
            return False, 0.0, f"Composite check failed — {details}"

        score = yakinlasma(model)
        return True, clamp_score(score), (
            f"All Bayes checks passed. yakinlasma = {score:.4f}"
        )

    return Constraint(name="valid_bayes_entry", check_fn=check)
