"""
mereoloji.axioms — CEM M1–M5 and teleological T1–T5 enforcement.

Classical Extensional Mereology (CEM):
  M1: Irreflexivity — ¬(x ⊏ x)
  M2: Asymmetry — (x ⊏ y) → ¬(y ⊏ x)
  M3: Transitivity — (x ⊏ y) ∧ (y ⊏ z) → (x ⊏ z)
  M4: Supplementation — (x ⊏ y) → ∃z(z ⊏ y ∧ ¬Overlap(z, x))
  M5: Fusion — for any non-empty set, a mereological sum exists

Teleological constraints (framework-specific):
  T1: Every part has a telos (purpose/function)
  T2: Telos propagation — parts inherit telos context from whole
  T3: Telos ordering — purposes form a hierarchy
  T4: Parts serving no telos are deficient (privation, KV₂)
  T5: Whole's telos is irreducible to sum of part-teloi
"""

from mereoloji.types import Part, Whole, PartOfEdge, Telos


# ---------------------------------------------------------------------------
# CEM M1–M5
# ---------------------------------------------------------------------------

def check_m1_irreflexivity(edges: list[PartOfEdge]) -> tuple[bool, list[str]]:
    """M1: ¬(x ⊏ x) — nothing is a proper part of itself.

    Note: PartOfEdge.__post_init__ prevents construction of reflexive edges,
    but this checks a collection for any that slipped through.
    """
    violations = [e for e in edges if e.part_name == e.whole_name]
    msgs = [f"M1: '{e.part_name}' is a part of itself" for e in violations]
    return len(violations) == 0, msgs


def check_m2_asymmetry(edges: list[PartOfEdge]) -> tuple[bool, list[str]]:
    """M2: (x ⊏ y) → ¬(y ⊏ x) — the part-of relation is asymmetric."""
    edge_set = {(e.part_name, e.whole_name) for e in edges}
    violations = []
    for p, w in edge_set:
        if (w, p) in edge_set:
            violations.append(f"M2: '{p}' ⊏ '{w}' AND '{w}' ⊏ '{p}'")
    return len(violations) == 0, violations


def check_m3_transitivity(
    edges: list[PartOfEdge],
    parts: dict[str, Part],
    wholes: dict[str, Whole],
) -> tuple[bool, list[str]]:
    """M3: (x ⊏ y) ∧ (y ⊏ z) → (x ⊏ z) — transitive closure check.

    Returns True if the explicit edge set is transitively closed.
    Returns missing edges as violation messages.
    """
    edge_set = {(e.part_name, e.whole_name) for e in edges}

    # Build adjacency for upward traversal: part → set of wholes
    children_of = {}  # whole_name → set of part_names
    for p, w in edge_set:
        children_of.setdefault(w, set()).add(p)

    # For each whole, find all transitive parts
    missing = []
    for e in edges:
        # If e.whole_name is itself a part of something, e.part_name
        # should also be a part of that higher whole
        for p2, w2 in list(edge_set):
            if p2 == e.whole_name:
                # e.part_name ⊏ e.whole_name ⊏ w2
                # So e.part_name ⊏ w2 should exist
                if (e.part_name, w2) not in edge_set:
                    missing.append(
                        f"M3: '{e.part_name}' ⊏ '{e.whole_name}' ⊏ '{w2}' "
                        f"but '{e.part_name}' ⊏ '{w2}' is missing"
                    )
    return len(missing) == 0, missing


def check_m4_supplementation(
    edges: list[PartOfEdge],
) -> tuple[bool, list[str]]:
    """M4: (x ⊏ y) → ∃z(z ⊏ y ∧ z ≠ x) — weak supplementation.

    If something has a proper part, it must have at least one OTHER proper part.
    This prevents atomic absorption (a whole with exactly one part that IS the whole).
    """
    # Group parts by whole
    parts_of: dict[str, set[str]] = {}
    for e in edges:
        parts_of.setdefault(e.whole_name, set()).add(e.part_name)

    violations = []
    for whole_name, part_names in parts_of.items():
        if len(part_names) == 1:
            only_part = next(iter(part_names))
            violations.append(
                f"M4: '{whole_name}' has only one part '{only_part}' — "
                f"supplementation requires at least one other part"
            )

    return len(violations) == 0, violations


def check_m5_fusion(
    parts: dict[str, Part],
    wholes: dict[str, Whole],
    edges: list[PartOfEdge],
) -> tuple[bool, list[str]]:
    """M5: For any non-empty set of parts, a mereological sum exists.

    In our finite model: every part that appears in an edge
    must belong to at least one whole.
    """
    parts_in_edges = {e.part_name for e in edges}
    wholes_in_edges = {e.whole_name for e in edges}

    orphans = []
    for part_name in parts:
        if part_name not in parts_in_edges and part_name not in wholes_in_edges:
            orphans.append(f"M5: Part '{part_name}' belongs to no whole")

    return len(orphans) == 0, orphans


# ---------------------------------------------------------------------------
# Teleological T1–T5
# ---------------------------------------------------------------------------

def check_t1_telos_present(parts: dict[str, Part]) -> tuple[bool, list[str]]:
    """T1: Every part has a telos.

    Enforced at Part construction, but double-checks collection.
    """
    violations = []
    for name, part in parts.items():
        if part.telos is None:
            violations.append(f"T1: Part '{name}' has no telos")
        elif not part.telos.name:
            violations.append(f"T1: Part '{name}' has empty telos name")
    return len(violations) == 0, violations


def check_t2_telos_propagation(
    parts: dict[str, Part],
    wholes: dict[str, Whole],
    edges: list[PartOfEdge],
) -> tuple[bool, list[str]]:
    """T2: Telos propagation — parts should serve the whole's telos context.

    Soft check: parts at 'local' level should have a containing whole
    at 'compositional' or 'global' level (telos hierarchy exists).
    """
    # Map part → its wholes
    part_to_wholes: dict[str, list[str]] = {}
    for e in edges:
        part_to_wholes.setdefault(e.part_name, []).append(e.whole_name)

    warnings = []
    for part_name, whole_names in part_to_wholes.items():
        part = parts.get(part_name)
        if part is None:
            continue
        if part.telos.level == "local":
            # Check that at least one enclosing whole has higher-level telos
            any_higher = False
            for wn in whole_names:
                w = wholes.get(wn)
                if w and w.telos.level in ("compositional", "global"):
                    any_higher = True
                    break
            if not any_higher and whole_names:
                warnings.append(
                    f"T2: Part '{part_name}' (local telos) has no whole "
                    f"with compositional/global telos"
                )

    return len(warnings) == 0, warnings


def check_t3_telos_ordering(
    parts: dict[str, Part],
    wholes: dict[str, Whole],
) -> tuple[bool, list[str]]:
    """T3: Telos ordering — purposes form a hierarchy.

    Checks that the structure contains all three telos levels.
    """
    levels = set()
    for p in parts.values():
        levels.add(p.telos.level)
    for w in wholes.values():
        levels.add(w.telos.level)

    missing = {"local", "compositional", "global"} - levels
    if missing:
        msgs = [f"T3: missing telos level(s): {missing}"]
        return False, msgs
    return True, []


def check_t4_no_purposeless(parts: dict[str, Part]) -> tuple[bool, list[str]]:
    """T4: Parts serving no telos are deficient (privation, KV₂).

    Equivalent to T1 but frames absence as privation (ontological deficiency,
    not positive entity — per KV₂ evil-as-absence).
    """
    violations = []
    for name, part in parts.items():
        if part.telos is None or not part.telos.name:
            violations.append(
                f"T4: Part '{name}' lacks telos — this is a privation (KV2: "
                f"deficiency is absence-of-good, not positive entity)"
            )
    return len(violations) == 0, violations


def check_t5_irreducible_telos(
    wholes: dict[str, Whole],
    parts: dict[str, Part],
    edges: list[PartOfEdge],
) -> tuple[bool, list[str]]:
    """T5: Whole's telos is irreducible to sum of part-teloi.

    Check: the whole's telos name should NOT be identical to any
    single part's telos. It must be a higher-order purpose.
    """
    # Map whole → its parts
    whole_to_parts: dict[str, list[str]] = {}
    for e in edges:
        whole_to_parts.setdefault(e.whole_name, []).append(e.part_name)

    violations = []
    for whole_name, part_names in whole_to_parts.items():
        w = wholes.get(whole_name)
        if w is None:
            continue
        part_telos_names = {
            parts[pn].telos.name for pn in part_names
            if pn in parts
        }
        if w.telos.name in part_telos_names:
            violations.append(
                f"T5: Whole '{whole_name}' telos '{w.telos.name}' is identical "
                f"to a part's telos — whole's telos must be irreducible"
            )
    return len(violations) == 0, violations


# ---------------------------------------------------------------------------
# Combined check
# ---------------------------------------------------------------------------

def check_all_cem(
    edges: list[PartOfEdge],
    parts: dict[str, Part],
    wholes: dict[str, Whole],
) -> dict[str, tuple[bool, list[str]]]:
    """Run all CEM M1–M5 checks. Returns {rule_name: (passed, messages)}."""
    return {
        "M1": check_m1_irreflexivity(edges),
        "M2": check_m2_asymmetry(edges),
        "M3": check_m3_transitivity(edges, parts, wholes),
        "M4": check_m4_supplementation(edges),
        "M5": check_m5_fusion(parts, wholes, edges),
    }


def check_all_teleological(
    parts: dict[str, Part],
    wholes: dict[str, Whole],
    edges: list[PartOfEdge],
) -> dict[str, tuple[bool, list[str]]]:
    """Run all teleological T1–T5 checks."""
    return {
        "T1": check_t1_telos_present(parts),
        "T2": check_t2_telos_propagation(parts, wholes, edges),
        "T3": check_t3_telos_ordering(parts, wholes),
        "T4": check_t4_no_purposeless(parts),
        "T5": check_t5_irreducible_telos(wholes, parts, edges),
    }
