"""
kavram_sozlugu.constraints — ai_assert constraint factories for the Ontology Lens.

These extend ai_assert.py's built-in constraints with ontology-specific checks.
Each factory returns an ai_assert.Constraint instance — the SAME type the
ai_assert engine already consumes. No modifications to ai_assert.py needed.

Constraints enforce:
  KV1:  Dual classification (harfi/ismi)
  KV4:  Convergence bound (0 < C < 1)
  KV8:  Name grounding (>= 1 Name)
  AX19: Name density (>= 20 for living)
  AX21: Continuous degrees (> 0)
  AX22: Degree < ekmel (never reaches supremum)
"""

import json
from ai_assert import Constraint


def kv1_classified() -> Constraint:
    """KV1: Output must contain dual_class = 'harfi' or 'ismi'."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "KV1: output is not valid JSON"

        dc = data.get("dual_class")
        if dc in ("harfi", "ismi"):
            return True, 0.99, f"KV1: classified as '{dc}'"
        if dc is None:
            return False, 0.0, "KV1: missing 'dual_class' field"
        return False, 0.1, f"KV1: invalid dual_class '{dc}', must be 'harfi' or 'ismi'"

    return Constraint(name="KV1 (harfi/ismi classification)", check_fn=check)


def kv8_grounded() -> Constraint:
    """KV8: Output must contain >= 1 Name mapping in hakikat."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "KV8: output is not valid JSON"

        hakikat = data.get("hakikat", [])
        if not isinstance(hakikat, list):
            return False, 0.0, "KV8: 'hakikat' must be a list"
        if len(hakikat) >= 1:
            return True, 0.99, f"KV8: grounded in {len(hakikat)} Name(s)"
        return False, 0.0, "KV8: hakikat is empty — concept must ground in >= 1 Name"

    return Constraint(name="KV8 (>= 1 Name grounding)", check_fn=check)


def name_density(min_count: int = 1) -> Constraint:
    """AX19: Output hakikat must have >= min_count Name entries.

    Use min_count=20 for living concepts.
    """
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "AX19: output is not valid JSON"

        hakikat = data.get("hakikat", [])
        if not isinstance(hakikat, list):
            return False, 0.0, "AX19: 'hakikat' must be a list"
        count = len(hakikat)
        if count >= min_count:
            score = min(0.5 + count / (min_count * 4), 0.9999)
            return True, score, f"AX19: {count} Names >= {min_count}"
        coverage = count / max(min_count, 1)
        return False, coverage * 0.5, f"AX19: {count} Names < {min_count}"

    return Constraint(name=f"AX19 (name_density >= {min_count})", check_fn=check)


def valid_tecelli_degrees() -> Constraint:
    """AX21+AX22: All degrees must be > 0 and finite."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "AX21/22: output is not valid JSON"

        hakikat = data.get("hakikat", [])
        if not isinstance(hakikat, list):
            return False, 0.0, "AX21/22: 'hakikat' must be a list"

        for i, entry in enumerate(hakikat):
            if not isinstance(entry, dict):
                return False, 0.0, f"AX21/22: hakikat[{i}] must be a dict"
            derece = entry.get("derece")
            if derece is None:
                return False, 0.0, f"AX21/22: hakikat[{i}] missing 'derece'"
            if not isinstance(derece, (int, float)):
                return False, 0.0, (
                    f"AX21/22: derece must be numeric, "
                    f"got {type(derece).__name__}"
                )
            if derece <= 0:
                return False, 0.0, f"AX21/22: derece must be > 0, got {derece}"

        return True, 0.99, f"AX21/22: all {len(hakikat)} degrees valid"

    return Constraint(name="AX21/22 (valid tecelli degrees)", check_fn=check)


def convergence_bound() -> Constraint:
    """KV4: Composite score must be strictly < 1.0. Flags >= 0.95 as warning (OR-4)."""
    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:
            # No convergence_score field — acceptable, not an error
            return True, 0.5, "KV4: no convergence_score field (acceptable)"

        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 — "
                f"STRUCTURAL ERROR (map claiming identity with territory)"
            )
        if score >= 0.95:
            return False, 0.1, (
                f"KV4: convergence_score = {score} >= 0.95 — "
                f"WARNING (OR-4: flag as potential error)"
            )
        if score > 0:
            return True, 0.99, (
                f"KV4: convergence_score = {score} in valid range (0, 1)"
            )
        return False, 0.0, f"KV4: convergence_score = {score} <= 0"

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


def names_in_registry(valid_names: set[str]) -> Constraint:
    """All Names referenced in the output must be known in the registry."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "names_in_registry: output is not valid JSON"

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

        unknown = []
        for entry in hakikat:
            if isinstance(entry, dict):
                isim = entry.get("isim", "")
                if isim not in valid_names:
                    unknown.append(isim)

        if not unknown:
            return True, 0.99, "All Names in registry"
        return False, 0.0, f"Unknown Names: {unknown}"

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


def valid_ontology_entry() -> Constraint:
    """Composite: valid JSON structure + KV1 + KV8 + valid degrees."""
    def check(output: str) -> tuple[bool, float, str]:
        try:
            data = json.loads(output)
        except json.JSONDecodeError:
            return False, 0.0, "Invalid ontology entry: not valid JSON"

        errors = []

        # Required fields
        if "name" not in data:
            errors.append("missing 'name'")

        # KV1
        dc = data.get("dual_class")
        if dc is None:
            errors.append("missing 'dual_class' (KV1)")
        elif dc not in ("harfi", "ismi"):
            errors.append(f"invalid dual_class: '{dc}'")

        # KV8 + AX21
        hakikat = data.get("hakikat", [])
        if not isinstance(hakikat, list) or len(hakikat) < 1:
            errors.append("hakikat must have >= 1 entry (KV8)")
        else:
            for i, entry in enumerate(hakikat):
                if not isinstance(entry, dict):
                    errors.append(f"hakikat[{i}] must be a dict")
                elif "isim" not in entry or "derece" not in entry:
                    errors.append(f"hakikat[{i}] missing 'isim' or 'derece'")
                elif (not isinstance(entry["derece"], (int, float))
                      or entry["derece"] <= 0):
                    errors.append(f"hakikat[{i}].derece must be > 0")

        if errors:
            return False, 0.0, "Invalid ontology entry: " + "; ".join(errors)

        score = min(0.5 + len(hakikat) * 0.05, 0.9999)
        return True, score, f"Valid ontology entry with {len(hakikat)} Names"

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