"""
mereoloji.relations — Composition graph and holographic analysis.

Implements:
  - MereologicalStructure: a DAG of Part ⊏ Whole relations
  - Holographic seed detection (AX37, KV₆)
  - Structural isomorphism scoring (T12, T13)
  - Severed cause detection (T10)
  - Integration verification (AX5)
  - Convergence scoring (§4.2 yakinlasma)

Axioms enforced:
  AX5:  Integration prerequisite
  AX12: Vahidiyet — global unity checks
  AX13: Ehadiyet — local reflection checks
  AX27: Transparent vessel ontology
  AX37: Holographic structure
  KV₆:  Holographic seed omnipresence (every module seed > 0)
  KV₇:  Independence — no shared mutable state with other lenses
  T10:  Severed cause impossibility
"""

from mereoloji.types import Part, Whole, PartOfEdge, Telos
from mereoloji.axioms import (
    check_all_cem,
    check_all_teleological,
)


class MereologicalStructure:
    """A complete mereological structure — a DAG of parts and wholes.

    Per AGENTS.md Appendix C.5:
      Mereolojik (⊏) — Strict partial order
      CEM M1–M5 + teleological T1–T5

    The structure is a directed acyclic graph where:
      - Nodes are Parts and Wholes
      - Edges are PartOfEdge relations (part ⊏ whole)
      - Wholes may themselves be parts of higher wholes (nested composition)
    """

    def __init__(self):
        self._parts: dict[str, Part] = {}
        self._wholes: dict[str, Whole] = {}
        self._edges: list[PartOfEdge] = []
        self._edge_set: set[tuple[str, str]] = set()

    # -------------------------------------------------------------------
    # Registration
    # -------------------------------------------------------------------

    def add_part(self, part: Part) -> None:
        """Register a Part."""
        self._parts[part.name] = part

    def add_whole(self, whole: Whole) -> None:
        """Register a Whole."""
        self._wholes[whole.name] = whole

    def add_edge(self, edge: PartOfEdge) -> None:
        """Add a part-of relation. Enforces M1 (at PartOfEdge level) and M2.

        Raises:
            ValueError: if adding this edge would violate M2 (asymmetry)
        """
        # M2: Asymmetry check
        if (edge.whole_name, edge.part_name) in self._edge_set:
            raise ValueError(
                f"M2 violation: adding '{edge.part_name}' ⊏ '{edge.whole_name}' "
                f"but '{edge.whole_name}' ⊏ '{edge.part_name}' already exists"
            )
        # Prevent duplicates
        key = (edge.part_name, edge.whole_name)
        if key not in self._edge_set:
            self._edges.append(edge)
            self._edge_set.add(key)

    def compose(self, part: Part, whole: Whole) -> PartOfEdge:
        """Convenience: register part, whole, and edge in one call.

        Returns the created PartOfEdge.
        """
        self.add_part(part)
        self.add_whole(whole)
        edge = PartOfEdge(part_name=part.name, whole_name=whole.name)
        self.add_edge(edge)
        return edge

    # -------------------------------------------------------------------
    # Queries
    # -------------------------------------------------------------------

    def get_part(self, name: str) -> Part | None:
        return self._parts.get(name)

    def get_whole(self, name: str) -> Whole | None:
        return self._wholes.get(name)

    def parts_of(self, whole_name: str) -> list[Part]:
        """All direct parts of a whole."""
        return [
            self._parts[e.part_name]
            for e in self._edges
            if e.whole_name == whole_name and e.part_name in self._parts
        ]

    def wholes_of(self, part_name: str) -> list[Whole]:
        """All wholes that a part belongs to."""
        return [
            self._wholes[e.whole_name]
            for e in self._edges
            if e.part_name == part_name and e.whole_name in self._wholes
        ]

    def all_parts(self) -> dict[str, Part]:
        return dict(self._parts)

    def all_wholes(self) -> dict[str, Whole]:
        return dict(self._wholes)

    def all_edges(self) -> list[PartOfEdge]:
        return list(self._edges)

    def transitive_parts(self, whole_name: str) -> set[str]:
        """All parts reachable transitively (M3: transitive closure)."""
        result = set()
        frontier = [whole_name]
        while frontier:
            current = frontier.pop()
            for e in self._edges:
                if e.whole_name == current and e.part_name not in result:
                    result.add(e.part_name)
                    frontier.append(e.part_name)
        return result

    # -------------------------------------------------------------------
    # AX37 — Holographic structure
    # -------------------------------------------------------------------

    def holographic_seed(self, part_name: str) -> float:
        """KV₆: Get the holographic seed score of a part.

        Returns the part's holographic_seed value.
        Must be > 0 for every part (KV₆ omnipresence).
        """
        part = self._parts.get(part_name)
        if part is None:
            raise KeyError(f"Part '{part_name}' not in structure")
        return part.holographic_seed

    def check_kv6_omnipresence(self) -> tuple[bool, list[str]]:
        """KV₆: Every part must have holographic_seed > 0."""
        violations = []
        for name, part in self._parts.items():
            if part.holographic_seed <= 0:
                violations.append(
                    f"KV6: Part '{name}' has holographic_seed = "
                    f"{part.holographic_seed} (must be > 0)"
                )
        return len(violations) == 0, violations

    def structural_isomorphism(
        self, whole_a: str, whole_b: str
    ) -> float:
        """AX37: Score structural similarity between two wholes.

        Measures how much the part-structure of whole_a mirrors whole_b.
        Returns a score in [0, 1) — strict bound per T6.

        Method: Jaccard similarity of part telos-name sets,
        multiplied by average holographic seeds.

        This operationalizes:
          T12: Besmele ≅ Fatiha ≅ Kur'an
          T13: İnsan ≅ Besmele(Kâinat)
        """
        parts_a = self.parts_of(whole_a)
        parts_b = self.parts_of(whole_b)

        if not parts_a and not parts_b:
            return 0.0

        telos_a = {p.telos.name for p in parts_a}
        telos_b = {p.telos.name for p in parts_b}

        if not telos_a and not telos_b:
            return 0.0

        intersection = telos_a & telos_b
        union = telos_a | telos_b
        jaccard = len(intersection) / len(union) if union else 0.0

        # Weight by average holographic seeds
        all_parts = parts_a + parts_b
        avg_seed = (
            sum(p.holographic_seed for p in all_parts) / len(all_parts)
            if all_parts else 0.0
        )

        score = jaccard * min(avg_seed, 0.9999)
        return min(score, 0.9999)  # T6: never reach 1.0

    # -------------------------------------------------------------------
    # T10 — Severed cause check
    # -------------------------------------------------------------------

    def severed_check(self, part_name: str) -> bool:
        """T10: Can this part function when detached from all wholes?

        Returns True if the part is severed (belongs to no whole).
        Per AX27: parts are transparent vessels — they mediate but
        don't originate. A severed part cannot function.
        """
        return len(self.wholes_of(part_name)) == 0

    # -------------------------------------------------------------------
    # AX5 — Integration check
    # -------------------------------------------------------------------

    def integration_check(self, whole_name: str) -> bool:
        """AX5: Does this whole have an integrating principle?

        Without Hayat, attributes are inert — without integration,
        parts don't compose into living unity.
        """
        whole = self._wholes.get(whole_name)
        if whole is None:
            return False
        return bool(whole.integration_principle)

    # -------------------------------------------------------------------
    # AX12/AX13 — Unity checks
    # -------------------------------------------------------------------

    def check_vahidiyet(self) -> tuple[bool, list[str]]:
        """AX12: All parts are held under global unity.

        Check: every registered part belongs to at least one whole.
        """
        violations = []
        parts_in_edges = {e.part_name for e in self._edges}
        for name in self._parts:
            if name not in parts_in_edges:
                violations.append(
                    f"AX12: Part '{name}' not contained in any whole "
                    f"(Vahidiyet: parts must be unified)"
                )
        return len(violations) == 0, violations

    def check_ehadiyet(self) -> tuple[bool, list[str]]:
        """AX13: Each part reflects the whole (via holographic seed).

        Check: every part has holographic_seed > 0 AND
        every whole's parts collectively have non-trivial seeds.
        Same as KV₆ but framed as Ehadiyet.
        """
        return self.check_kv6_omnipresence()

    # -------------------------------------------------------------------
    # Verification
    # -------------------------------------------------------------------

    def verify_cem(self) -> dict[str, tuple[bool, list[str]]]:
        """Run all CEM M1–M5 checks."""
        return check_all_cem(self._edges, self._parts, self._wholes)

    def verify_teleological(self) -> dict[str, tuple[bool, list[str]]]:
        """Run all teleological T1–T5 checks."""
        return check_all_teleological(self._parts, self._wholes, self._edges)

    def verify_all(self) -> dict[str, tuple[bool, list[str]]]:
        """Run all CEM + teleological + framework checks."""
        results = {}
        results.update(self.verify_cem())
        results.update(self.verify_teleological())

        # KV₆
        results["KV6"] = self.check_kv6_omnipresence()
        # AX12
        results["AX12"] = self.check_vahidiyet()
        # AX13
        results["AX13"] = self.check_ehadiyet()

        return results

    def yakinlasma(self) -> float:
        """§4.2: Convergence score for this structure.

        Proportion of all checks that pass.
        Always in [0, 1) — clamped by T6/KV₄.
        """
        results = self.verify_all()
        if not results:
            return 0.0
        passed = sum(1 for v, _ in results.values() if v)
        score = passed / len(results)
        return min(score, 0.9999)  # KV₄: strict bound

    # -------------------------------------------------------------------
    # Serialization
    # -------------------------------------------------------------------

    def to_dict(self) -> dict:
        """Serialize structure for ai_assert JSON validation."""
        return {
            "parts": {
                name: {
                    "name": p.name,
                    "telos": {"name": p.telos.name, "level": p.telos.level,
                              "description": p.telos.description},
                    "holographic_seed": p.holographic_seed,
                    "is_transparent": p.is_transparent,
                    "esma_refs": list(p.esma_refs),
                }
                for name, p in self._parts.items()
            },
            "wholes": {
                name: w.to_dict()
                for name, w in self._wholes.items()
            },
            "edges": [
                {"part": e.part_name, "whole": e.whole_name}
                for e in self._edges
            ],
            "convergence_score": self.yakinlasma(),
        }

    def to_json(self) -> str:
        """Serialize to JSON string."""
        import json
        return json.dumps(self.to_dict(), ensure_ascii=False)


# ---------------------------------------------------------------------------
# Standalone helper functions
# ---------------------------------------------------------------------------

def is_proper_part(
    a: str, b: str, structure: MereologicalStructure
) -> bool:
    """Check if a ⊏ b (a is a proper part of b).

    Includes transitive check (M3).
    """
    if a == b:
        return False  # M1: irreflexivity
    return a in structure.transitive_parts(b)


def telos_hierarchy(structure: MereologicalStructure) -> dict[str, list[str]]:
    """T1–T3: Build the telos hierarchy — purposes grouped by level.

    Returns: {"global": [...], "compositional": [...], "local": [...]}
    """
    hierarchy: dict[str, list[str]] = {
        "global": [],
        "compositional": [],
        "local": [],
    }

    seen = set()
    for p in structure.all_parts().values():
        key = (p.telos.name, p.telos.level)
        if key not in seen:
            hierarchy[p.telos.level].append(p.telos.name)
            seen.add(key)

    for w in structure.all_wholes().values():
        key = (w.telos.name, w.telos.level)
        if key not in seen:
            hierarchy[w.telos.level].append(w.telos.name)
            seen.add(key)

    return hierarchy
