"""
kategori_teorisi.verification — Categorical verification engine.

Implements the structural checks from AGENTS.md §4.3 and §5.4:
  - Category validity (identity, composition closure)
  - Functor faithfulness & fullness (KV₅)
  - Naturality square verification
  - AX12 (Vahidiyet) connectedness
  - AX13 (Ehadiyet) local reflection
  - yakinlasma convergence score for the KategoriTeorisi lens

Design constraints:
  - KV₇: No imports from other lenses.
  - KV₄/T6: All scores ∈ [0, 1).
"""

from __future__ import annotations

from collections import deque
from typing import Optional

from .types import (
    Category,
    CatObject,
    Functor,
    MorphismKind,
    NaturalTransformation,
    clamp_score,
)


# ========================================================================
# Category validity
# ========================================================================

def has_identities(cat: Category) -> tuple[bool, list[str]]:
    """Check every object has an identity morphism.

    Returns (all_ok, list_of_objects_missing_identity).
    """
    missing: list[str] = []
    for obj in cat.objects:
        if cat.identity_for(obj) is None:
            missing.append(obj.name)
    return len(missing) == 0, missing


def check_composition_defined(
    cat: Category,
) -> tuple[bool, list[tuple[str, str]]]:
    """Check that every composable pair (f: A→B, g: B→C) has a
    declared composite g∘f in the category.

    Identity compositions (id_B ∘ f = f, g ∘ id_B = g) are
    automatically satisfied and not required in the composition table.

    Returns (all_defined, list_of_missing_pairs).
    """
    missing: list[tuple[str, str]] = []
    for f in cat.morphisms:
        if f.is_identity:
            continue
        for g in cat.morphisms:
            if g.is_identity:
                continue
            if f.target == g.source and f.source != g.target:
                # f: A→B, g: B→C — need g∘f
                if cat.compose(f.name, g.name) is None:
                    missing.append((f.name, g.name))
    return len(missing) == 0, missing


def is_valid_category(cat: Category) -> tuple[bool, list[str]]:
    """Full validity check: identities + composition closure.

    Returns (valid, list_of_issues).
    """
    issues: list[str] = []
    ok_id, missing_id = has_identities(cat)
    if not ok_id:
        issues.append(f"Missing identities: {missing_id}")
    ok_comp, missing_comp = check_composition_defined(cat)
    if not ok_comp:
        for f_name, g_name in missing_comp:
            issues.append(f"Composition undefined: {g_name} ∘ {f_name}")
    return len(issues) == 0, issues


# ========================================================================
# Functor checks (KV₅)
# ========================================================================

def check_functor_objects(func: Functor) -> tuple[bool, list[str]]:
    """Verify every source-category object is mapped.

    Returns (all_mapped, list_of_unmapped_object_names).
    """
    unmapped: list[str] = []
    for obj in func.source_cat.objects:
        if func.F_obj(obj.name) is None:
            unmapped.append(obj.name)
    return len(unmapped) == 0, unmapped


def check_functor_morphisms(func: Functor) -> tuple[bool, list[str]]:
    """Verify every source-category morphism is mapped.

    Returns (all_mapped, list_of_unmapped_morphism_names).
    """
    unmapped: list[str] = []
    for m in func.source_cat.morphisms:
        if func.F_mor(m.name) is None:
            unmapped.append(m.name)
    return len(unmapped) == 0, unmapped


def is_faithful(func: Functor) -> tuple[bool, list[str]]:
    """KV₅ — Faithful functor check.

    F is faithful iff for every pair of objects (A, B) in C,
    the action F : Hom(A,B) → Hom(F(A),F(B)) is injective.

    Returns (faithful, list_of_hom_pair_violations).
    """
    violations: list[str] = []
    src_cat = func.source_cat
    for a in src_cat.objects:
        for b in src_cat.objects:
            hom_ab = src_cat.hom(a, b)
            if len(hom_ab) < 2:
                continue
            # Check injectivity: distinct morphisms must map to distinct images
            images: dict[str, str] = {}  # image_name → original_name
            for m in hom_ab:
                img = func.F_mor(m.name)
                if img is None:
                    continue
                if img in images:
                    violations.append(
                        f"Hom({a.name},{b.name}): "
                        f"{m.name} and {images[img]} both map to {img}"
                    )
                else:
                    images[img] = m.name
    return len(violations) == 0, violations


def is_full(func: Functor) -> tuple[bool, list[str]]:
    """Full functor check.

    F is full iff for every pair (A, B) in C,
    F : Hom(A,B) → Hom(F(A),F(B)) is surjective.

    Returns (full, list_of_hom_pair_violations).
    """
    violations: list[str] = []
    src_cat = func.source_cat
    tgt_cat = func.target_cat
    for a in src_cat.objects:
        for b in src_cat.objects:
            fa_name = func.F_obj(a.name)
            fb_name = func.F_obj(b.name)
            if fa_name is None or fb_name is None:
                continue
            fa = tgt_cat.get_object(fa_name)
            fb = tgt_cat.get_object(fb_name)
            if fa is None or fb is None:
                continue
            # Images of source hom-set
            src_images = set()
            for m in src_cat.hom(a, b):
                img = func.F_mor(m.name)
                if img is not None:
                    src_images.add(img)
            # Target hom-set
            tgt_hom = tgt_cat.hom(fa, fb)
            uncovered = {m.name for m in tgt_hom} - src_images
            if uncovered:
                violations.append(
                    f"Hom(F({a.name}),F({b.name})): uncovered = {uncovered}"
                )
    return len(violations) == 0, violations


def check_identity_preservation(func: Functor) -> tuple[bool, list[str]]:
    """Functor must map identity to identity: F(id_A) = id_{F(A)}."""
    violations: list[str] = []
    for obj in func.source_cat.objects:
        src_id = func.source_cat.identity_for(obj)
        if src_id is None:
            continue
        img = func.F_mor(src_id.name)
        if img is None:
            violations.append(f"id_{obj.name} not mapped")
            continue
        fa_name = func.F_obj(obj.name)
        if fa_name is None:
            continue
        fa = func.target_cat.get_object(fa_name)
        if fa is None:
            continue
        tgt_id = func.target_cat.identity_for(fa)
        if tgt_id is None or img != tgt_id.name:
            violations.append(
                f"F(id_{obj.name}) = {img}, but id_{{F({obj.name})}} = "
                f"{tgt_id.name if tgt_id else 'None'}"
            )
    return len(violations) == 0, violations


def verify_functor(func: Functor) -> dict:
    """Full KV₅ functor verification.

    Returns a diagnostic dict:
      objects_mapped, morphisms_mapped, faithful, full,
      identity_preservation, issues.
    """
    ok_obj, unmapped_obj = check_functor_objects(func)
    ok_mor, unmapped_mor = check_functor_morphisms(func)
    ok_faith, faith_violations = is_faithful(func)
    ok_full, full_violations = is_full(func)
    ok_id, id_violations = check_identity_preservation(func)

    issues: list[str] = []
    if not ok_obj:
        issues.append(f"Unmapped objects: {unmapped_obj}")
    if not ok_mor:
        issues.append(f"Unmapped morphisms: {unmapped_mor}")
    if not ok_faith:
        issues.extend(faith_violations)
    if not ok_id:
        issues.extend(id_violations)

    return {
        "objects_mapped": ok_obj,
        "morphisms_mapped": ok_mor,
        "faithful": ok_faith,
        "full": ok_full,
        "identity_preservation": ok_id,
        "issues": issues,
        "passed": ok_obj and ok_mor and ok_faith and ok_id,
    }


# ========================================================================
# Natural Transformation verification
# ========================================================================

def check_naturality(eta: NaturalTransformation) -> tuple[bool, list[str]]:
    """Verify naturality squares for η: F ⇒ G.

    For every f: A → B in the source category:
      G(f) ∘ η_A  =  η_B ∘ F(f)

    Since we use named compositions, we check:
      1. η_A and η_B are defined as components.
      2. F(f) and G(f) are defined as morphism maps.
      3. The compositions match in the declared table.

    If composition is not declared for a pair, we skip the
    strict algebraic check but log a warning.

    Returns (naturality_ok, list_of_issues).
    """
    F = eta.source_functor
    G = eta.target_functor
    src_cat = F.source_cat
    tgt_cat = F.target_cat
    issues: list[str] = []

    for mor in src_cat.non_identity_morphisms():
        a_name = mor.source.name
        b_name = mor.target.name

        # Components
        eta_a = eta.components.get(a_name)
        eta_b = eta.components.get(b_name)
        if eta_a is None:
            issues.append(f"Missing component η_{a_name}")
            continue
        if eta_b is None:
            issues.append(f"Missing component η_{b_name}")
            continue

        # Functor images
        f_f = F.F_mor(mor.name)  # F(f)
        g_f = G.F_mor(mor.name)  # G(f)
        if f_f is None:
            issues.append(f"F({mor.name}) not mapped")
            continue
        if g_f is None:
            issues.append(f"G({mor.name}) not mapped")
            continue

        # Composition check: G(f) ∘ η_A  vs  η_B ∘ F(f)
        # Left side
        left = tgt_cat.compose(eta_a, g_f)
        # Right side
        right = tgt_cat.compose(f_f, eta_b)

        if left is None or right is None:
            # Can't verify — composition not declared
            issues.append(
                f"Naturality square for {mor.name}: composition "
                f"not fully declared (left={left}, right={right})"
            )
        elif left != right:
            issues.append(
                f"Naturality FAILS for {mor.name}: "
                f"G(f)∘η_A = {left} ≠ {right} = η_B∘F(f)"
            )

    return len(issues) == 0, issues


def check_component_coverage(eta: NaturalTransformation) -> tuple[bool, list[str]]:
    """Every object in the source category must have a component."""
    missing: list[str] = []
    for obj in eta.source_category.objects:
        if obj.name not in eta.components:
            missing.append(obj.name)
    return len(missing) == 0, missing


def verify_natural_transformation(eta: NaturalTransformation) -> dict:
    """Full verification of a natural transformation.

    Returns diagnostic dict.
    """
    ok_cov, missing_cov = check_component_coverage(eta)
    ok_nat, nat_issues = check_naturality(eta)

    issues: list[str] = []
    if not ok_cov:
        issues.append(f"Missing components: {missing_cov}")
    issues.extend(nat_issues)

    return {
        "component_coverage": ok_cov,
        "naturality": ok_nat,
        "issues": issues,
        "passed": ok_cov and ok_nat,
    }


# ========================================================================
# AX12 — Vahidiyet (Global Unity / Connectedness)
# ========================================================================

def check_vahidiyet(cat: Category) -> tuple[bool, float, str]:
    """AX12 — Verify that the category is connected:
    every pair of objects can be linked by a chain of morphisms
    (ignoring direction).

    Returns (connected, score, message).
    Score = fraction of objects reachable from the first object.
    """
    objs = list(cat.objects)
    if len(objs) <= 1:
        return True, clamp_score(1.0), "AX12: trivially connected (≤1 object)"

    # Build undirected adjacency from morphisms
    adj: dict[str, set[str]] = {o.name: set() for o in objs}
    for m in cat.morphisms:
        if not m.is_identity:
            adj[m.source.name].add(m.target.name)
            adj[m.target.name].add(m.source.name)

    # BFS from first object
    start = objs[0].name
    visited: set[str] = set()
    queue: deque[str] = deque([start])
    while queue:
        node = queue.popleft()
        if node in visited:
            continue
        visited.add(node)
        for nb in adj.get(node, set()):
            if nb not in visited:
                queue.append(nb)

    ratio = len(visited) / len(objs)
    score = clamp_score(ratio)
    if ratio >= 1.0:
        return True, score, "AX12 Vahidiyet: category is connected"
    else:
        unreachable = sorted(set(o.name for o in objs) - visited)
        return (
            False,
            score,
            f"AX12 Vahidiyet: {len(unreachable)} unreachable objects: "
            f"{unreachable}",
        )


# ========================================================================
# AX13 — Ehadiyet (Local Reflection)
# ========================================================================

def check_ehadiyet(cat: Category) -> tuple[bool, float, str]:
    """AX13 — Verify that each object reflects the whole:
    every object must have at least one non-identity morphism
    connecting it to another object.

    Score = fraction of objects with ≥1 non-identity connection.
    """
    objs = list(cat.objects)
    if len(objs) <= 1:
        return True, clamp_score(1.0), "AX13: trivially reflective (≤1 object)"

    connected_count = 0
    isolated: list[str] = []
    for obj in objs:
        has_connection = any(
            m for m in cat.morphisms
            if not m.is_identity and (m.source == obj or m.target == obj)
        )
        if has_connection:
            connected_count += 1
        else:
            isolated.append(obj.name)

    ratio = connected_count / len(objs)
    score = clamp_score(ratio)
    if ratio >= 1.0:
        return True, score, "AX13 Ehadiyet: all objects reflect the whole"
    else:
        return (
            False,
            score,
            f"AX13 Ehadiyet: {len(isolated)} isolated objects: {isolated}",
        )


# ========================================================================
# Yakinlasma — Convergence score for KategoriTeorisi lens
# ========================================================================

def yakinlasma(
    cat: Optional[Category] = None,
    functor: Optional[Functor] = None,
    eta: Optional[NaturalTransformation] = None,
) -> float:
    """Compute the convergence score for the category theory lens.

    Weighted composite of available checks:
      - Category validity:       weight 0.20
      - Vahidiyet (AX12):        weight 0.15
      - Ehadiyet (AX13):         weight 0.15
      - Functor verification:    weight 0.30
      - Natural transformation:  weight 0.20

    Returns a score in [0, 1) per T6/KV₄.
    """
    total_weight = 0.0
    weighted_sum = 0.0

    if cat is not None:
        # Category validity
        valid, _ = is_valid_category(cat)
        cat_score = 0.95 if valid else 0.3
        weighted_sum += 0.20 * cat_score
        total_weight += 0.20

        # Vahidiyet
        _, vah_score, _ = check_vahidiyet(cat)
        weighted_sum += 0.15 * vah_score
        total_weight += 0.15

        # Ehadiyet
        _, eha_score, _ = check_ehadiyet(cat)
        weighted_sum += 0.15 * eha_score
        total_weight += 0.15

    if functor is not None:
        result = verify_functor(functor)
        # Score based on how many checks pass
        checks = [
            result["objects_mapped"],
            result["morphisms_mapped"],
            result["faithful"],
            result["identity_preservation"],
        ]
        func_score = sum(1 for c in checks if c) / len(checks) * 0.95
        weighted_sum += 0.30 * func_score
        total_weight += 0.30

    if eta is not None:
        result = verify_natural_transformation(eta)
        nat_score = 0.95 if result["passed"] else 0.3
        weighted_sum += 0.20 * nat_score
        total_weight += 0.20

    if total_weight == 0.0:
        return 0.0

    raw = weighted_sum / total_weight
    return clamp_score(raw)


# ========================================================================
# Framework summary
# ========================================================================

def framework_summary() -> dict:
    """Return metadata about the KategoriTeorisi lens capabilities."""
    return {
        "lens": "KategoriTeorisi",
        "lens_number": 6,
        "faculty": "Sır",
        "domain": "Functor F: Rep→Hakikat, η: Vahidiyet⇒Ehadiyet",
        "axioms_checked": ["AX12", "AX13"],
        "kavaid_checked": ["KV4", "KV5", "KV7"],
        "capabilities": [
            "category_validity",
            "functor_faithfulness",
            "functor_fullness",
            "identity_preservation",
            "naturality_verification",
            "vahidiyet_connectedness",
            "ehadiyet_reflection",
            "yakinlasma_score",
        ],
        "max_score": 0.9999,
    }
