"""
fol_formalizasyon.constraints — ai_assert constraint factories for the FOL Lens.

Each factory returns an ai_assert.Constraint instance.
Constraints enforce:
  AX1:  Core axiom — formal ordering
  AX65: Reference pairing — every formalization traces to source
  KV₁:  Harfî/İsmî dual classification
  KV₄:  Convergence bound 0 < C < 1
  KV₇:  Independence — no shared state
  KV₈:  Esmâ grounding — every concept maps to ≥1 Name
  T6:   ConvergenceScore < 1.0
  T17:  Coverage ≤ 6/7
"""

import json
from ai_assert import Constraint
from .axioms import (
    defined_axiom_ids, gap_axiom_ids,
    list_all_axioms, list_all_theorems, list_all_kavaid,
    check_dependency_closure,
)
from .inference import extract_axiom_refs


def axiom_coverage() -> Constraint:
    """Axiom coverage: fraction of defined axioms referenced.

    Expects JSON with 'text' field containing analysis text.
    Score based on how many of the 59 defined axioms are referenced.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "axiom_coverage: output is not valid JSON"

        text = data.get("text", "")
        if not isinstance(text, str):
            return False, 0.0, "axiom_coverage: 'text' must be a string"

        if not text:
            return False, 0.0, "axiom_coverage: empty text"

        refs = extract_axiom_refs(text)
        found = len(refs["axioms"])
        total = len(defined_axiom_ids())

        if found == 0:
            return False, 0.0, "axiom_coverage: no axiom references found"

        ratio = found / total if total > 0 else 0.0
        score = min(ratio, 0.9999)
        return True, score, (
            f"axiom_coverage: {found}/{total} axioms referenced "
            f"({score:.4f})"
        )

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


def theorem_derivable() -> Constraint:
    """Theorem derivability: theorems referenced must have dependencies met.

    Expects JSON with 'text' field.
    Checks that for each referenced theorem, its dependency axioms
    are also referenced (per the Inference Graph).
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "theorem_derivable: not valid JSON"

        text = data.get("text", "")
        if not isinstance(text, str):
            return False, 0.0, "theorem_derivable: 'text' must be a string"

        refs = extract_axiom_refs(text)
        theorems = refs["theorems"]
        axioms_set = set(refs["axioms"])

        if not theorems:
            return True, 0.5, "theorem_derivable: no theorems referenced"

        violations = []
        for t_id in theorems:
            ok, missing = check_dependency_closure(t_id, axioms_set)
            if not ok:
                violations.append(f"{t_id} missing deps: {missing}")

        if violations:
            ratio = 1 - len(violations) / len(theorems)
            score = min(max(ratio * 0.5, 0.0), 0.9999)
            return False, score, (
                f"theorem_derivable: {len(violations)} violations: "
                f"{violations}"
            )

        score = min(0.5 + len(theorems) * 0.05, 0.9999)
        return True, score, (
            f"theorem_derivable: all {len(theorems)} theorems "
            f"have dependencies met"
        )

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


def kavaid_all_pass() -> Constraint:
    """KV₁–KV₈: All 8 kavaid must be referenced/acknowledged.

    Expects JSON with 'kavaid_status' dict mapping KV1..KV8 to bool.
    Per AX52: multiplicative gate — any False collapses the whole.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "kavaid_all_pass: not valid JSON"

        status = data.get("kavaid_status", {})
        if not isinstance(status, dict):
            return False, 0.0, "kavaid_all_pass: 'kavaid_status' must be dict"

        all_kv = list_all_kavaid()
        total = len(all_kv)

        if not status:
            return False, 0.0, "kavaid_all_pass: no kavaid_status provided"

        passed = 0
        failed = []
        for kv in all_kv:
            key = kv.id
            val = status.get(key, False)
            if val:
                passed += 1
            else:
                failed.append(key)

        if failed:
            # AX52: multiplicative gate — zero in any = collapse
            return False, 0.0, (
                f"kavaid_all_pass: {len(failed)} failed (multiplicative "
                f"gate): {failed}"
            )

        score = min(0.5 + passed * 0.06, 0.9999)
        return True, score, f"kavaid_all_pass: all {passed}/{total} passed"

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


def well_formed_formulas() -> Constraint:
    """All formulas must be syntactically well-formed.

    Expects JSON with 'formulas' list of formula representations.
    Each entry: {"predicate": str, "arity": int, "terms_count": int}.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "well_formed: not valid JSON"

        formulas = data.get("formulas", [])
        if not isinstance(formulas, list):
            return False, 0.0, "well_formed: 'formulas' must be a list"

        if not formulas:
            return True, 0.5, "well_formed: no formulas to check"

        errors = []
        for i, f in enumerate(formulas):
            if not isinstance(f, dict):
                errors.append(f"formula[{i}]: not a dict")
                continue
            arity = f.get("arity", 0)
            terms = f.get("terms_count", 0)
            if arity != terms:
                errors.append(
                    f"formula[{i}]: arity={arity} != terms={terms}"
                )

        if errors:
            return False, 0.1, f"well_formed: {errors}"

        score = min(0.5 + len(formulas) * 0.05, 0.9999)
        return True, score, (
            f"well_formed: all {len(formulas)} formulas well-formed"
        )

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


def fol_convergence_bound() -> Constraint:
    """KV₄/T6: FOL convergence score must be strictly < 1.0.

    Expects JSON with 'convergence_score' numeric field.
    Flags >= 0.95 as warning per OR-4.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "KV4: not valid JSON"

        score = data.get("convergence_score")
        if score is None:
            return True, 0.5, "KV4: no convergence_score field"

        if not isinstance(score, (int, float)):
            return False, 0.0, "KV4: convergence_score must be numeric"

        if score >= 1.0:
            return False, 0.0, (
                f"KV4: convergence_score = {score} >= 1.0 — "
                f"STRUCTURAL ERROR (T6)"
            )
        if score >= 0.95:
            return False, 0.1, (
                f"KV4: convergence_score = {score} >= 0.95 — "
                f"WARNING (OR-4: flag as error)"
            )
        if score > 0:
            return True, 0.99, (
                f"KV4: convergence_score = {score} in valid range"
            )
        return False, 0.0, f"KV4: convergence_score = {score} <= 0"

    return Constraint(name="KV4 (FOL convergence bound)", check_fn=check)


def sort_grounded() -> Constraint:
    """All terms must use valid Sort values from Appendix C.1.

    Expects JSON with 'terms' list of {"name": str, "sort": str}.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "sort_grounded: not valid JSON"

        terms = data.get("terms", [])
        if not isinstance(terms, list):
            return False, 0.0, "sort_grounded: 'terms' must be a list"

        if not terms:
            return True, 0.5, "sort_grounded: no terms to check"

        from .types import Sort
        valid_sorts = {s.value for s in Sort}

        invalid = []
        for t in terms:
            if not isinstance(t, dict):
                continue
            sort_name = t.get("sort", "")
            if sort_name not in valid_sorts:
                invalid.append(f"{t.get('name', '?')}:{sort_name}")

        if invalid:
            return False, 0.1, (
                f"sort_grounded: invalid sorts: {invalid}"
            )

        score = min(0.5 + len(terms) * 0.05, 0.9999)
        return True, score, (
            f"sort_grounded: all {len(terms)} terms have valid sorts"
        )

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


def valid_fol_entry() -> Constraint:
    """Composite: axiom coverage + well-formed + KV₄ + sort grounding.

    Expects JSON with 'text', 'formulas', 'terms', 'convergence_score'.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "valid_fol_entry: not valid JSON"

        errors = []

        # axiom refs present?
        text = data.get("text", "")
        if isinstance(text, str) and text:
            refs = extract_axiom_refs(text)
            if not refs["axioms"]:
                errors.append("no axiom references in text")

        # formulas well-formed?
        formulas = data.get("formulas", [])
        if isinstance(formulas, list):
            for i, f in enumerate(formulas):
                if isinstance(f, dict):
                    arity = f.get("arity", 0)
                    terms = f.get("terms_count", 0)
                    if arity != terms:
                        errors.append(
                            f"formula[{i}] arity mismatch"
                        )

        # convergence bound
        score_val = data.get("convergence_score")
        if isinstance(score_val, (int, float)):
            if score_val >= 1.0:
                errors.append(
                    f"KV4: convergence={score_val} >= 1.0"
                )
            elif score_val >= 0.95:
                errors.append(
                    f"KV4: convergence={score_val} >= 0.95 (OR-4)"
                )

        # sort grounding
        from .types import Sort
        valid_sorts = {s.value for s in Sort}
        terms_list = data.get("terms", [])
        if isinstance(terms_list, list):
            for t in terms_list:
                if isinstance(t, dict):
                    s = t.get("sort", "")
                    if s and s not in valid_sorts:
                        errors.append(f"invalid sort: {s}")

        if errors:
            return False, len(errors) / (len(errors) + 5), (
                f"valid_fol_entry violations: {errors}"
            )

        score = min(0.6 + len(data) * 0.05, 0.9999)
        return True, score, "valid_fol_entry: all checks passed"

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