"""
kategori_teorisi.types — Core categorical types for the KategoriTeorisi Lens.

Implements the mathematical structures from AGENTS.md §4.3:
  - Category (objects + morphisms + composition + identity)
  - Functor  (structure-preserving map between categories)
  - NaturalTransformation (component-wise map between functors)

Design constraints (§4.2):
  - KV₇ (İhlâs): No imports from other lenses.
  - All scores in [0, 1) per T6/KV₄.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class MorphismKind(Enum):
    """Classification of morphisms in the framework."""
    IDENTITY = "identity"
    DERIVATION = "derivation"       # logical derivation in Rep
    DEPENDENCY = "dependency"       # structural dependency
    TECELLI = "tecelli"             # manifestation (Hakikat side)
    KASKAD = "kaskad"               # cascade (AX23)
    DELALET = "delalet"             # indication (C₆ chain)
    COMPOSITION = "composition"     # composite morphism


class FunctorProperty(Enum):
    """Properties a functor may possess (KV₅)."""
    FAITHFUL = "faithful"     # injective on hom-sets
    FULL = "full"             # surjective on hom-sets
    ESSENTIALLY_SURJECTIVE = "essentially_surjective"
    NATURAL = "natural"       # commutes with structural maps


class CategoryRole(Enum):
    """Role of a category within the framework (§4.3)."""
    REP = "Rep"             # Representation category (formal system)
    HAKIKAT = "Hakikat"     # Reality category (Name-governed truth)
    CUSTOM = "Custom"       # User-defined


# ---------------------------------------------------------------------------
# Core types
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class CatObject:
    """An object in a category.

    Attributes:
        name: Unique identifier within the category.
        sort: Optional ontological sort (e.g. 'S_Isim', 'S_Sifat').
        tags: Metadata tags for classification.
    """
    name: str
    sort: str = ""
    tags: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if not self.name:
            raise ValueError("CatObject.name must be non-empty")


@dataclass(frozen=True)
class Morphism:
    """A morphism (arrow) between two objects.

    Attributes:
        name: Human-readable identifier.
        source: Domain object.
        target: Codomain object.
        kind: Classification tag from MorphismKind.
    """
    name: str
    source: CatObject
    target: CatObject
    kind: MorphismKind = MorphismKind.DERIVATION

    def __post_init__(self) -> None:
        if not self.name:
            raise ValueError("Morphism.name must be non-empty")

    @property
    def is_identity(self) -> bool:
        return self.source == self.target and self.kind == MorphismKind.IDENTITY

    @property
    def is_endomorphism(self) -> bool:
        return self.source == self.target


@dataclass
class Category:
    """A (small, finite) category.

    Enforces:
      - Every object has an identity morphism.
      - Composition is defined for compatible morphisms.
      - AX12 (Vahidiyet): connectedness — all objects reachable.
      - AX13 (Ehadiyet): local reflection metric.

    Attributes:
        name: Category identifier.
        role: Structural role (Rep / Hakikat / Custom).
        objects: Frozenset of CatObject.
        morphisms: Frozenset of Morphism (including identities).
    """
    name: str
    role: CategoryRole = CategoryRole.CUSTOM
    _objects: set[CatObject] = field(default_factory=set, repr=False)
    _morphisms: set[Morphism] = field(default_factory=set, repr=False)
    _composition: dict[tuple[str, str], str] = field(
        default_factory=dict, repr=False
    )

    # -- mutators (build phase) -------------------------------------------

    def add_object(self, obj: CatObject) -> None:
        """Add an object and auto-create its identity morphism."""
        self._objects.add(obj)
        identity = Morphism(
            name=f"id_{obj.name}",
            source=obj,
            target=obj,
            kind=MorphismKind.IDENTITY,
        )
        self._morphisms.add(identity)

    def add_morphism(self, m: Morphism) -> None:
        """Add a morphism. Source and target must already exist."""
        if m.source not in self._objects:
            raise ValueError(
                f"Morphism source '{m.source.name}' not in category"
            )
        if m.target not in self._objects:
            raise ValueError(
                f"Morphism target '{m.target.name}' not in category"
            )
        self._morphisms.add(m)

    def set_composition(self, f_name: str, g_name: str, gf_name: str) -> None:
        """Declare composition: g ∘ f = gf (g after f).

        f: A → B, g: B → C  ⟹  gf: A → C.
        """
        self._composition[(f_name, g_name)] = gf_name

    # -- queries -----------------------------------------------------------

    @property
    def objects(self) -> frozenset[CatObject]:
        return frozenset(self._objects)

    @property
    def morphisms(self) -> frozenset[Morphism]:
        return frozenset(self._morphisms)

    def get_object(self, name: str) -> Optional[CatObject]:
        for o in self._objects:
            if o.name == name:
                return o
        return None

    def get_morphism(self, name: str) -> Optional[Morphism]:
        for m in self._morphisms:
            if m.name == name:
                return m
        return None

    def hom(self, a: CatObject, b: CatObject) -> frozenset[Morphism]:
        """Hom(a, b) — all morphisms from *a* to *b*."""
        return frozenset(
            m for m in self._morphisms
            if m.source == a and m.target == b
        )

    def compose(self, f_name: str, g_name: str) -> Optional[str]:
        """Return the name of g ∘ f if declared, else None."""
        return self._composition.get((f_name, g_name))

    def identity_for(self, obj: CatObject) -> Optional[Morphism]:
        """Return the identity morphism on *obj*."""
        for m in self._morphisms:
            if m.source == obj and m.target == obj and m.kind == MorphismKind.IDENTITY:
                return m
        return None

    def object_count(self) -> int:
        return len(self._objects)

    def morphism_count(self) -> int:
        return len(self._morphisms)

    def non_identity_morphisms(self) -> frozenset[Morphism]:
        return frozenset(m for m in self._morphisms if not m.is_identity)

    # -- serialisation -----------------------------------------------------

    def to_dict(self) -> dict:
        """JSON-friendly summary."""
        return {
            "name": self.name,
            "role": self.role.value,
            "object_count": self.object_count(),
            "morphism_count": self.morphism_count(),
            "objects": sorted(o.name for o in self._objects),
            "morphisms": [
                {
                    "name": m.name,
                    "source": m.source.name,
                    "target": m.target.name,
                    "kind": m.kind.value,
                }
                for m in sorted(self._morphisms, key=lambda m: m.name)
            ],
        }


# ---------------------------------------------------------------------------
# Functor
# ---------------------------------------------------------------------------

@dataclass
class Functor:
    """A functor F: C → D between two categories.

    AGENTS.md §4.3:
      F : Rep → Hakikat  (representation to reality)
      Must be faithful (KV₅) and natural.

    Attributes:
        name: Human-readable name.
        source_cat: Domain category C.
        target_cat: Codomain category D.
        object_map:   {obj_name_in_C: obj_name_in_D}
        morphism_map: {mor_name_in_C: mor_name_in_D}
    """
    name: str
    source_cat: Category
    target_cat: Category
    object_map: dict[str, str] = field(default_factory=dict)
    morphism_map: dict[str, str] = field(default_factory=dict)

    def map_object(self, src_name: str, tgt_name: str) -> None:
        """Declare F(src_obj) = tgt_obj."""
        if self.source_cat.get_object(src_name) is None:
            raise ValueError(f"Object '{src_name}' not in source category")
        if self.target_cat.get_object(tgt_name) is None:
            raise ValueError(f"Object '{tgt_name}' not in target category")
        self.object_map[src_name] = tgt_name

    def map_morphism(self, src_name: str, tgt_name: str) -> None:
        """Declare F(src_mor) = tgt_mor."""
        if self.source_cat.get_morphism(src_name) is None:
            raise ValueError(
                f"Morphism '{src_name}' not in source category"
            )
        if self.target_cat.get_morphism(tgt_name) is None:
            raise ValueError(
                f"Morphism '{tgt_name}' not in target category"
            )
        self.morphism_map[src_name] = tgt_name

    def F_obj(self, obj_name: str) -> Optional[str]:
        """Return F(obj) name, or None if unmapped."""
        return self.object_map.get(obj_name)

    def F_mor(self, mor_name: str) -> Optional[str]:
        """Return F(mor) name, or None if unmapped."""
        return self.morphism_map.get(mor_name)

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "source": self.source_cat.name,
            "target": self.target_cat.name,
            "object_map": dict(self.object_map),
            "morphism_map": dict(self.morphism_map),
        }


# ---------------------------------------------------------------------------
# Natural Transformation
# ---------------------------------------------------------------------------

@dataclass
class NaturalTransformation:
    """A natural transformation η: F ⇒ G between two functors F, G: C → D.

    AGENTS.md §4.3:
      η : Vahidiyet ⇒ Ehadiyet  (global unity → local reflection)

    For each object A in C, η provides a component morphism
      η_A : F(A) → G(A)  in D
    such that for every morphism f: A → B in C:
      G(f) ∘ η_A  =  η_B ∘ F(f)   (naturality square commutes)

    Attributes:
        name: Human-readable name.
        source_functor: F
        target_functor: G
        components: {obj_name_in_C: morphism_name_in_D}
    """
    name: str
    source_functor: Functor
    target_functor: Functor
    components: dict[str, str] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if self.source_functor.source_cat is not self.target_functor.source_cat:
            raise ValueError(
                "Source and target functors must share the same source category"
            )
        if self.source_functor.target_cat is not self.target_functor.target_cat:
            raise ValueError(
                "Source and target functors must share the same target category"
            )

    def set_component(self, obj_name: str, mor_name: str) -> None:
        """Declare η_A = mor for object A in the source category."""
        src_cat = self.source_functor.source_cat
        tgt_cat = self.source_functor.target_cat
        if src_cat.get_object(obj_name) is None:
            raise ValueError(
                f"Object '{obj_name}' not in source category"
            )
        if tgt_cat.get_morphism(mor_name) is None:
            raise ValueError(
                f"Morphism '{mor_name}' not in target category"
            )
        self.components[obj_name] = mor_name

    @property
    def source_category(self) -> Category:
        return self.source_functor.source_cat

    @property
    def target_category(self) -> Category:
        return self.source_functor.target_cat

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "source_functor": self.source_functor.name,
            "target_functor": self.target_functor.name,
            "components": dict(self.components),
        }


# ---------------------------------------------------------------------------
# Convergence helpers
# ---------------------------------------------------------------------------

def clamp_score(value: float) -> float:
    """Clamp a convergence score to [0, 1) per T6/KV₄."""
    if value < 0.0:
        return 0.0
    if value >= 1.0:
        return 0.9999
    return value
