"""
mereoloji.constraints — ai_assert constraint factories for the Mereology Lens.

These extend ai_assert.py's built-in constraints with mereology-specific checks.
Each factory returns an ai_assert.Constraint instance.

Constraints enforce:
  CEM M1–M5:  Classical Extensional Mereology
  KV₆:        Holographic seed omnipresence
  T1:         Every part has a telos
  AX5:        Integration principle required
  AX29:       Nizam ∧ İntizam (dual order)
"""

import json
from ai_assert import Constraint


def cem_valid() -> Constraint:
    """CEM M1–M5: All classical mereology axioms satisfied.

    Expects JSON with 'edges' array of {part, whole} objects.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "CEM: output is not valid JSON"

        edges = data.get("edges", [])
        if not isinstance(edges, list):
            return False, 0.0, "CEM: 'edges' must be a list"

        # M1: Irreflexivity
        for e in edges:
            if not isinstance(e, dict):
                return False, 0.0, "CEM: each edge must be a dict"
            if e.get("part") == e.get("whole"):
                return False, 0.0, (
                    f"M1: '{e.get('part')}' is a part of itself"
                )

        # M2: Asymmetry
        edge_pairs = {(e["part"], e["whole"]) for e in edges if "part" in e and "whole" in e}
        for p, w in edge_pairs:
            if (w, p) in edge_pairs:
                return False, 0.1, f"M2: '{p}' ⊏ '{w}' AND '{w}' ⊏ '{p}'"

        # M4: Supplementation — each whole should have >= 2 parts
        parts_per_whole: dict[str, int] = {}
        for e in edges:
            w = e.get("whole", "")
            parts_per_whole[w] = parts_per_whole.get(w, 0) + 1
        for w, count in parts_per_whole.items():
            if count == 1:
                return False, 0.3, (
                    f"M4: whole '{w}' has only 1 part — supplementation "
                    f"requires at least 2"
                )

        score = min(0.5 + len(edges) * 0.05, 0.9999)
        return True, score, f"CEM: {len(edges)} edges, all axioms satisfied"

    return Constraint(name="CEM (M1-M5)", check_fn=check)


def holographic_omnipresence() -> Constraint:
    """KV₆: Every part must have holographic_seed > 0."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "KV6: output is not valid JSON"

        parts = data.get("parts", {})
        if not isinstance(parts, dict):
            return False, 0.0, "KV6: 'parts' must be a dict"

        if not parts:
            return True, 0.5, "KV6: no parts to check"

        violations = []
        for name, part_data in parts.items():
            seed = part_data.get("holographic_seed", 0)
            if not isinstance(seed, (int, float)) or seed <= 0:
                violations.append(name)

        if violations:
            return False, 0.0, (
                f"KV6: parts with seed <= 0: {violations}"
            )

        total = sum(
            p.get("holographic_seed", 0) for p in parts.values()
        )
        avg = total / len(parts)
        score = min(0.5 + avg * 0.4, 0.9999)
        return True, score, (
            f"KV6: all {len(parts)} parts have seed > 0 (avg={avg:.4f})"
        )

    return Constraint(name="KV6 (holographic omnipresence)", check_fn=check)


def telos_complete() -> Constraint:
    """T1: Every part must have a non-empty telos."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "T1: output is not valid JSON"

        parts = data.get("parts", {})
        if not isinstance(parts, dict):
            return False, 0.0, "T1: 'parts' must be a dict"

        if not parts:
            return True, 0.5, "T1: no parts to check"

        no_telos = []
        for name, part_data in parts.items():
            telos = part_data.get("telos")
            if telos is None:
                no_telos.append(name)
            elif isinstance(telos, dict) and not telos.get("name"):
                no_telos.append(name)

        if no_telos:
            return False, 0.0, f"T1: parts without telos: {no_telos}"

        return True, 0.99, f"T1: all {len(parts)} parts have telos"

    return Constraint(name="T1 (telos completeness)", check_fn=check)


def integration_present() -> Constraint:
    """AX5: At least one whole must have a non-empty integration_principle."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "AX5: output is not valid JSON"

        wholes = data.get("wholes", {})
        if not isinstance(wholes, dict):
            return False, 0.0, "AX5: 'wholes' must be a dict"

        if not wholes:
            return False, 0.0, "AX5: no wholes defined"

        integrated = 0
        for name, whole_data in wholes.items():
            ip = whole_data.get("integration_principle", "")
            if ip:
                integrated += 1

        if integrated == 0:
            return False, 0.0, (
                "AX5: no whole has an integration_principle — "
                "without Hayat, parts remain inert"
            )

        score = min(0.5 + integrated * 0.1, 0.9999)
        return True, score, (
            f"AX5: {integrated}/{len(wholes)} wholes have integration principle"
        )

    return Constraint(name="AX5 (integration prerequisite)", check_fn=check)


def dual_order() -> Constraint:
    """AX29: Wholes must have both nizam (quantitative) and intizam (qualitative) > 0.

    CosmicOrder = Nizam ∧ İntizam — multiplicative gate.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "AX29: output is not valid JSON"

        wholes = data.get("wholes", {})
        if not isinstance(wholes, dict):
            return False, 0.0, "AX29: 'wholes' must be a dict"

        if not wholes:
            return True, 0.5, "AX29: no wholes to check"

        violations = []
        for name, whole_data in wholes.items():
            nizam = whole_data.get("nizam_score", 0)
            intizam = whole_data.get("intizam_score", 0)
            if nizam <= 0 or intizam <= 0:
                violations.append(
                    f"'{name}' (nizam={nizam}, intizam={intizam})"
                )

        if violations:
            return False, 0.1, (
                f"AX29: wholes with zero order dimension: {violations}"
            )

        return True, 0.99, f"AX29: all {len(wholes)} wholes have dual order > 0"

    return Constraint(name="AX29 (Nizam ∧ İntizam)", check_fn=check)


def convergence_bound() -> Constraint:
    """KV₄: convergence_score must be strictly < 1.0. Flags >= 0.95 as warning."""
    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"

        score = data.get("convergence_score")
        if score is None:
            return True, 0.5, "KV4: no convergence_score field"

        if not isinstance(score, (int, float)):
            return False, 0.0, "KV4: convergence_score must be numeric"

        if score >= 1.0:
            return False, 0.0, (
                f"KV4: convergence_score = {score} >= 1.0 — STRUCTURAL ERROR"
            )
        if score >= 0.95:
            return False, 0.1, (
                f"KV4: convergence_score = {score} >= 0.95 — WARNING (OR-4)"
            )
        if score > 0:
            return True, 0.99, f"KV4: convergence_score = {score} in valid range"
        return False, 0.0, f"KV4: convergence_score = {score} <= 0"

    return Constraint(name="KV4 (mereology convergence bound)", check_fn=check)


def valid_mereological_entry() -> Constraint:
    """Composite: CEM + KV₆ + T1 + AX5 + AX29 on JSON structure."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "Invalid mereological entry: not valid JSON"

        errors = []

        # Must have parts and wholes
        parts = data.get("parts", {})
        wholes = data.get("wholes", {})
        edges = data.get("edges", [])

        if not isinstance(parts, dict):
            errors.append("'parts' must be a dict")
        if not isinstance(wholes, dict):
            errors.append("'wholes' must be a dict")
        if not isinstance(edges, list):
            errors.append("'edges' must be a list")

        if errors:
            return False, 0.0, f"Structure errors: {errors}"

        # KV₆: holographic seeds
        for name, p in parts.items():
            seed = p.get("holographic_seed", 0) if isinstance(p, dict) else 0
            if seed <= 0:
                errors.append(f"KV6: '{name}' seed <= 0")

        # T1: telos present
        for name, p in parts.items():
            if isinstance(p, dict):
                telos = p.get("telos")
                if not telos or (isinstance(telos, dict) and not telos.get("name")):
                    errors.append(f"T1: '{name}' has no telos")

        # AX5: integration
        any_integrated = False
        for name, w in wholes.items():
            if isinstance(w, dict) and w.get("integration_principle"):
                any_integrated = True
                break
        if wholes and not any_integrated:
            errors.append("AX5: no whole has integration_principle")

        # M1: irreflexivity
        for e in edges:
            if isinstance(e, dict) and e.get("part") == e.get("whole"):
                errors.append(f"M1: '{e.get('part')}' ⊏ itself")

        if errors:
            return False, len(errors) / (len(errors) + 5), f"Violations: {errors}"

        score = min(0.5 + len(parts) * 0.05 + len(edges) * 0.05, 0.9999)
        return True, score, "Valid mereological entry"

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