"""
kategori_teorisi.constraints — ai_assert constraint factories for the KategoriTeorisi Lens.

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

Constraints enforce:
  - KV₅:  Functor faithfulness & identity preservation.
  - KV₄:  Convergence bound < 1.0 (T6).
  - AX12: Vahidiyet (connectedness / global unity).
  - AX13: Ehadiyet (local reflection).
  - KV₇:  Independence — no shared state with other lenses.
"""

from __future__ import annotations

import json

from ai_assert import Constraint

from .types import (
    Category,
    CategoryRole,
    CatObject,
    Functor,
    Morphism,
    MorphismKind,
    NaturalTransformation,
    clamp_score,
)
from .verification import (
    check_ehadiyet,
    check_vahidiyet,
    is_faithful,
    is_valid_category,
    verify_functor,
    verify_natural_transformation,
)


# ========================================================================
# Helpers — rebuild objects from JSON
# ========================================================================

def _parse_category(data: dict) -> Category:
    """Reconstruct a Category from a JSON-friendly dict.

    Expected schema:
    {
      "name": str,
      "role": "Rep" | "Hakikat" | "Custom",
      "objects": [{"name": str, "sort": str, "tags": [...]}],
      "morphisms": [{"name": str, "source": str, "target": str, "kind": str}],
      "compositions": [{"f": str, "g": str, "gf": str}]  (optional)
    }
    """
    role = CategoryRole(data.get("role", "Custom"))
    cat = Category(name=data["name"], role=role)

    # Objects
    obj_lookup: dict[str, CatObject] = {}
    for o in data.get("objects", []):
        co = CatObject(
            name=o["name"],
            sort=o.get("sort", ""),
            tags=tuple(o.get("tags", ())),
        )
        cat.add_object(co)
        obj_lookup[co.name] = co

    # Morphisms
    for m in data.get("morphisms", []):
        kind = MorphismKind(m.get("kind", "derivation"))
        if kind == MorphismKind.IDENTITY:
            continue  # auto-created by add_object
        src = obj_lookup.get(m["source"])
        tgt = obj_lookup.get(m["target"])
        if src is None or tgt is None:
            continue
        cat.add_morphism(Morphism(name=m["name"], source=src, target=tgt, kind=kind))

    # Compositions
    for c in data.get("compositions", []):
        cat.set_composition(c["f"], c["g"], c["gf"])

    return cat


def _parse_functor(data: dict, src_cat: Category, tgt_cat: Category) -> Functor:
    """Reconstruct a Functor from JSON.

    Expected schema:
    {
      "name": str,
      "object_map": {"src_obj": "tgt_obj", ...},
      "morphism_map": {"src_mor": "tgt_mor", ...}
    }
    """
    func = Functor(name=data["name"], source_cat=src_cat, target_cat=tgt_cat)
    for s, t in data.get("object_map", {}).items():
        func.map_object(s, t)
    for s, t in data.get("morphism_map", {}).items():
        func.map_morphism(s, t)
    return func


# ========================================================================
# Constraint factories
# ========================================================================

def category_validity() -> Constraint:
    """Category must have identities and composition closure.

    Expects JSON with 'category' key (category dict).
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "category_validity: output is not valid JSON"

        cat_data = data.get("category")
        if cat_data is None:
            return False, 0.0, "category_validity: missing 'category' key"

        try:
            cat = _parse_category(cat_data)
        except (KeyError, ValueError) as e:
            return False, 0.0, f"category_validity: parse error — {e}"

        valid, issues = is_valid_category(cat)
        if valid:
            score = clamp_score(0.95)
            return True, score, "category_validity: valid category"
        else:
            return False, 0.1, f"category_validity: {'; '.join(issues)}"

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


def functor_faithfulness() -> Constraint:
    """KV₅ — Functor must be faithful (injective on hom-sets).

    Expects JSON with 'source_category', 'target_category', 'functor' keys.
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "KV5: output is not valid JSON"

        try:
            src_cat = _parse_category(data["source_category"])
            tgt_cat = _parse_category(data["target_category"])
            func = _parse_functor(data["functor"], src_cat, tgt_cat)
        except (KeyError, ValueError) as e:
            return False, 0.0, f"KV5: parse error — {e}"

        result = verify_functor(func)
        if result["faithful"] and result["identity_preservation"]:
            score = clamp_score(0.95)
            return True, score, "KV5: functor is faithful with identity preservation"
        else:
            issues = result["issues"]
            return False, 0.1, f"KV5: {'; '.join(issues)}"

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


def vahidiyet_containment() -> Constraint:
    """AX12 — All objects connected under Vahidiyet (global unity).

    Expects JSON with 'category' key.
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "AX12: output is not valid JSON"

        cat_data = data.get("category")
        if cat_data is None:
            return False, 0.0, "AX12: missing 'category' key"

        try:
            cat = _parse_category(cat_data)
        except (KeyError, ValueError) as e:
            return False, 0.0, f"AX12: parse error — {e}"

        ok, score, msg = check_vahidiyet(cat)
        return ok, score, msg

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


def ehadiyet_reflection() -> Constraint:
    """AX13 — Every object reflects the whole (local reflection).

    Expects JSON with 'category' key.
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "AX13: output is not valid JSON"

        cat_data = data.get("category")
        if cat_data is None:
            return False, 0.0, "AX13: missing 'category' key"

        try:
            cat = _parse_category(cat_data)
        except (KeyError, ValueError) as e:
            return False, 0.0, f"AX13: parse error — {e}"

        ok, score, msg = check_ehadiyet(cat)
        return ok, score, msg

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


def naturality_check() -> Constraint:
    """η: F ⇒ G must satisfy naturality squares.

    Expects JSON with 'source_category', 'target_category',
    'source_functor', 'target_functor', 'transformation' keys.
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "naturality: output is not valid JSON"

        try:
            src_cat = _parse_category(data["source_category"])
            tgt_cat = _parse_category(data["target_category"])
            F = _parse_functor(data["source_functor"], src_cat, tgt_cat)
            G = _parse_functor(data["target_functor"], src_cat, tgt_cat)
            eta = NaturalTransformation(
                name=data["transformation"]["name"],
                source_functor=F,
                target_functor=G,
            )
            for obj_name, mor_name in data["transformation"].get(
                "components", {}
            ).items():
                eta.set_component(obj_name, mor_name)
        except (KeyError, ValueError) as e:
            return False, 0.0, f"naturality: parse error — {e}"

        result = verify_natural_transformation(eta)
        if result["passed"]:
            score = clamp_score(0.95)
            return True, score, "naturality: all squares commute"
        else:
            issues = result["issues"]
            return False, 0.1, f"naturality: {'; '.join(issues)}"

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


def category_convergence_bound() -> Constraint:
    """KV₄/T6 — Convergence score must be strictly < 1.0.

    Expects JSON with optional 'convergence_score' key.
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "KV4: output is not valid JSON"

        raw_score = data.get("convergence_score", 0.0)
        if not isinstance(raw_score, (int, float)):
            return False, 0.0, "KV4: convergence_score must be numeric"

        if raw_score >= 1.0:
            return (
                False,
                0.0,
                f"KV4/T6 VIOLATION: score {raw_score} ≥ 1.0"
            )
        if raw_score >= 0.95:
            return (
                False,
                0.0,
                f"KV4 WARNING: score {raw_score} ≥ 0.95 — likely error"
            )

        score = clamp_score(raw_score)
        return True, score, f"KV4: convergence score {score:.4f} < 1.0"

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


def valid_category_entry() -> Constraint:
    """Composite constraint — verifies a complete categorical analysis entry.

    Expects JSON with:
      - 'category': category dict
      - 'convergence_score': float < 1.0
      - Optionally 'source_category', 'target_category', 'functor'
    """

    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "valid_category_entry: not valid JSON"

        issues: list[str] = []
        sub_scores: list[float] = []

        # 1. Category present and valid
        cat_data = data.get("category")
        if cat_data is None:
            issues.append("missing 'category'")
        else:
            try:
                cat = _parse_category(cat_data)
                valid, cat_issues = is_valid_category(cat)
                if valid:
                    sub_scores.append(0.95)
                else:
                    issues.extend(cat_issues)
                    sub_scores.append(0.2)
            except (KeyError, ValueError) as e:
                issues.append(f"category parse error: {e}")
                sub_scores.append(0.0)

        # 2. Convergence bound
        raw = data.get("convergence_score", 0.0)
        if isinstance(raw, (int, float)) and raw < 1.0:
            sub_scores.append(clamp_score(raw))
        else:
            issues.append(f"convergence_score invalid or ≥ 1.0: {raw}")
            sub_scores.append(0.0)

        # 3. Optional functor
        if "functor" in data and "source_category" in data and "target_category" in data:
            try:
                src_cat = _parse_category(data["source_category"])
                tgt_cat = _parse_category(data["target_category"])
                func = _parse_functor(data["functor"], src_cat, tgt_cat)
                result = verify_functor(func)
                if result["passed"]:
                    sub_scores.append(0.95)
                else:
                    sub_scores.append(0.2)
                    issues.extend(result["issues"])
            except (KeyError, ValueError) as e:
                issues.append(f"functor parse error: {e}")
                sub_scores.append(0.0)

        if not sub_scores:
            return False, 0.0, "valid_category_entry: no data to evaluate"

        avg = sum(sub_scores) / len(sub_scores)
        score = clamp_score(avg)

        if issues:
            return False, score, f"valid_category_entry: {'; '.join(issues)}"
        return True, score, f"valid_category_entry: all checks passed ({score:.4f})"

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