"""
fol_formalizasyon.types — Core data types for the FOL Lens (Lens #3).

Implements first-order logic building blocks from AGENTS.md:
  - Sort      — The 16 sorts from Appendix C.1 (Type Universe)
  - Variable  — Typed variable bound to a sort
  - Term      — Constants, variables, and function applications
  - Predicate — Formal predicates with typed signatures (Appendix C.3)
  - Formula   — Logical formulas: Atomic, Not, And, Or, Implies, ForAll, Exists
  - Axiom     — Named formula with source reference (AX64/AX65 compliance)
  - Theorem   — Derived formula with dependency list

Governing axioms enforced at construction:
  AX1:  Every formula references the ontological ordering
  AX64: Source text immutability — axiom text is frozen
  AX65: Reference pairing — every axiom has a source ref
  KV₇:  Independence — no imports from other lens packages
"""

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


# ---------------------------------------------------------------------------
# Sort — Type Universe (AGENTS.md Appendix C.1)
# ---------------------------------------------------------------------------

@unique
class Sort(Enum):
    """The 16 sorts of the framework's type universe.

    Per AGENTS.md Appendix C.1, each sort has a symbol and cardinality:
      S_Zat (|1|), S_Sifat (|7|), S_Isim (|≤1001|), S_Fiil (|∞|),
      S_Eser (|∞|), S_Seun (|finite|), S_Mumkin (|—|), S_Vacib (|1|),
      S_Latife (|7|), S_Mercek (|7|), S_Mertebe (|4|), S_Ortam (|3|),
      S_Makam (|4|), S_Text (|—|), S_Kavram (|—|), Being (universal sort).
    """
    S_ZAT = "S_Zat"
    S_SIFAT = "S_Sifat"
    S_ISIM = "S_Isim"
    S_FIIL = "S_Fiil"
    S_ESER = "S_Eser"
    S_SEUN = "S_Seun"
    S_MUMKIN = "S_Mumkin"
    S_VACIB = "S_Vacib"
    S_LATIFE = "S_Latife"
    S_MERCEK = "S_Mercek"
    S_MERTEBE = "S_Mertebe"
    S_ORTAM = "S_Ortam"
    S_MAKAM = "S_Makam"
    S_TEXT = "S_Text"
    S_KAVRAM = "S_Kavram"
    BEING = "Being"  # universal sort for AX1


# Cardinality constraints per Appendix C.1
SORT_CARDINALITY: dict[Sort, Optional[int]] = {
    Sort.S_ZAT: 1,
    Sort.S_SIFAT: 7,
    Sort.S_ISIM: 1001,    # upper bound
    Sort.S_FIIL: None,    # infinite
    Sort.S_ESER: None,    # infinite
    Sort.S_SEUN: None,    # finite but unnamed
    Sort.S_MUMKIN: None,  # unbounded
    Sort.S_VACIB: 1,
    Sort.S_LATIFE: 7,
    Sort.S_MERCEK: 7,
    Sort.S_MERTEBE: 4,
    Sort.S_ORTAM: 3,
    Sort.S_MAKAM: 4,
    Sort.S_TEXT: None,
    Sort.S_KAVRAM: None,
    Sort.BEING: None,
}


# ---------------------------------------------------------------------------
# Variable — Typed variable
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Variable:
    """A typed variable in a given sort.

    Attributes:
        name: Variable name (e.g. 'x', 'n', 's')
        sort: The sort this variable ranges over
    """
    name: str
    sort: Sort

    def __post_init__(self):
        if not self.name:
            raise ValueError("Variable must have a non-empty name")

    def __repr__(self) -> str:
        return f"{self.name}:{self.sort.value}"


# ---------------------------------------------------------------------------
# Term — Constants, variables, function applications
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Constant:
    """A named constant of a given sort.

    Attributes:
        name:  Constant name (e.g. 'Hayat', 'el-Hakim')
        sort:  The sort this constant belongs to
    """
    name: str
    sort: Sort

    def __post_init__(self):
        if not self.name:
            raise ValueError("Constant must have a non-empty name")

    def __repr__(self) -> str:
        return self.name


@dataclass(frozen=True)
class FunctionApplication:
    """Application of a function symbol to arguments.

    Per AGENTS.md Appendix C.4: functions have typed signatures.

    Attributes:
        func_name:   Function name (e.g. 'sifat_of', 'mertebe')
        args:        Tuple of terms (constants, variables, or nested applications)
        result_sort: The sort of the function's output
    """
    func_name: str
    args: tuple  # tuple of Term values
    result_sort: Sort

    def __post_init__(self):
        if not self.func_name:
            raise ValueError("FunctionApplication must have a non-empty func_name")

    def __repr__(self) -> str:
        arg_strs = ", ".join(repr(a) for a in self.args)
        return f"{self.func_name}({arg_strs})"


# Term is a union type
Term = Union[Variable, Constant, FunctionApplication]


# ---------------------------------------------------------------------------
# Predicate — Formal predicate with typed signature
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Predicate:
    """A formal predicate with typed argument slots.

    Per AGENTS.md Appendix C.3: predicates have explicit signatures.
    Example: Sudur: S_Fiil × S_Zat → Bool

    Attributes:
        name:       Predicate name (e.g. 'Sudur', 'Tecelli', 'Mumkin')
        arg_sorts:  Tuple of sorts for each argument position
        reading:    Human-readable description of the predicate's meaning
    """
    name: str
    arg_sorts: tuple[Sort, ...]
    reading: str = ""

    def __post_init__(self):
        if not self.name:
            raise ValueError("Predicate must have a non-empty name")
        if not self.arg_sorts:
            raise ValueError(
                f"Predicate '{self.name}' must have at least one argument sort"
            )

    @property
    def arity(self) -> int:
        """Number of argument positions."""
        return len(self.arg_sorts)

    def __repr__(self) -> str:
        sorts_str = " × ".join(s.value for s in self.arg_sorts)
        return f"{self.name}: {sorts_str} → Bool"


# ---------------------------------------------------------------------------
# Formula — Logical formulas (AST)
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Atomic:
    """Atomic formula: predicate applied to terms.

    P(t₁, t₂, ..., tₙ) where P is a predicate and tᵢ are terms.
    """
    predicate: Predicate
    terms: tuple[Term, ...]

    def __post_init__(self):
        if len(self.terms) != self.predicate.arity:
            raise ValueError(
                f"Predicate '{self.predicate.name}' expects {self.predicate.arity} "
                f"args, got {len(self.terms)}"
            )

    def __repr__(self) -> str:
        terms_str = ", ".join(repr(t) for t in self.terms)
        return f"{self.predicate.name}({terms_str})"


@dataclass(frozen=True)
class Equals:
    """Equality formula: t₁ = t₂."""
    left: Term
    right: Term

    def __repr__(self) -> str:
        return f"({self.left!r} = {self.right!r})"


@dataclass(frozen=True)
class Not:
    """Negation: ¬φ."""
    operand: 'Formula'

    def __repr__(self) -> str:
        return f"¬({self.operand!r})"


@dataclass(frozen=True)
class And:
    """Conjunction: φ₁ ∧ φ₂."""
    left: 'Formula'
    right: 'Formula'

    def __repr__(self) -> str:
        return f"({self.left!r} ∧ {self.right!r})"


@dataclass(frozen=True)
class Or:
    """Disjunction: φ₁ ∨ φ₂."""
    left: 'Formula'
    right: 'Formula'

    def __repr__(self) -> str:
        return f"({self.left!r} ∨ {self.right!r})"


@dataclass(frozen=True)
class Implies:
    """Implication: φ₁ → φ₂."""
    antecedent: 'Formula'
    consequent: 'Formula'

    def __repr__(self) -> str:
        return f"({self.antecedent!r} → {self.consequent!r})"


@dataclass(frozen=True)
class ForAll:
    """Universal quantification: ∀v.φ."""
    variable: Variable
    body: 'Formula'

    def __repr__(self) -> str:
        return f"∀{self.variable!r}.({self.body!r})"


@dataclass(frozen=True)
class Exists:
    """Existential quantification: ∃v.φ."""
    variable: Variable
    body: 'Formula'

    def __repr__(self) -> str:
        return f"∃{self.variable!r}.({self.body!r})"


# Formula is a union of all formula types
Formula = Union[Atomic, Equals, Not, And, Or, Implies, ForAll, Exists]


# ---------------------------------------------------------------------------
# Axiom — Named formula with source reference (AX64, AX65)
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Axiom:
    """A named axiom from the framework, with source reference.

    Per AGENTS.md:
      AX64: Source text is immutable — axiom_text is frozen
      AX65: Every formalization has a source reference

    Attributes:
        id:          Axiom identifier (e.g. 'AX1', 'AX17', 'AX64')
        name:        Human-readable name (e.g. 'Ontological Axiom')
        formula:     The formal FOL representation (or None for non-formalizable)
        axiom_text:  Original text from AGENTS.md (AX64: immutable)
        source_ref:  Source chunk reference (AX65: mandatory)
        layer:       Which AGENTS.md layer (1-7) this axiom belongs to
        tags:        Classification tags (e.g. ('foundational', 'modal'))
    """
    id: str
    name: str
    formula: Optional[Formula]
    axiom_text: str
    source_ref: str
    layer: int
    tags: tuple[str, ...] = ()

    def __post_init__(self):
        if not self.id:
            raise ValueError("Axiom must have a non-empty id")
        if not self.source_ref:
            raise ValueError(
                f"Axiom '{self.id}' must have a source_ref (AX65)"
            )
        if self.layer < 1 or self.layer > 7:
            raise ValueError(
                f"Axiom '{self.id}' layer must be 1–7, got {self.layer}"
            )


# ---------------------------------------------------------------------------
# Theorem — Derived formula with proof dependencies
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Theorem:
    """A derived theorem with dependency list.

    Per AGENTS.md Appendix B.2: theorems derive from axioms.

    Attributes:
        id:           Theorem identifier (e.g. 'T3', 'T15')
        name:         Human-readable name
        formula:      The formal FOL representation (or None)
        statement:    Original statement text (AX64)
        source_ref:   Source chunk reference (AX65)
        layer:        Which AGENTS.md layer (1-7)
        depends_on:   Tuple of axiom/theorem IDs this depends on
    """
    id: str
    name: str
    formula: Optional[Formula]
    statement: str
    source_ref: str
    layer: int
    depends_on: tuple[str, ...] = ()

    def __post_init__(self):
        if not self.id:
            raise ValueError("Theorem must have a non-empty id")
        if not self.source_ref:
            raise ValueError(
                f"Theorem '{self.id}' must have a source_ref (AX65)"
            )


# ---------------------------------------------------------------------------
# Kavaid — Formal constraint rule
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Kavaid:
    """A formal constraint rule from the framework.

    Per AGENTS.md §5.4: The 8 kavaid that every analysis must satisfy.

    Attributes:
        id:          Kavaid identifier (e.g. 'KV1' through 'KV8')
        name:        Human-readable name
        constraint:  Constraint text
        source_ref:  Source chunk reference (AX65)
        is_critical: Whether this is a CRITICAL kavaid (KV₄, KV₇)
    """
    id: str
    name: str
    constraint: str
    source_ref: str
    is_critical: bool = False

    def __post_init__(self):
        if not self.id:
            raise ValueError("Kavaid must have a non-empty id")
        if not self.source_ref:
            raise ValueError(
                f"Kavaid '{self.id}' must have a source_ref (AX65)"
            )


# ---------------------------------------------------------------------------
# Helper: well-formedness check
# ---------------------------------------------------------------------------

def is_well_formed(formula: Formula) -> bool:
    """Check if a formula is well-formed (all types match).

    Returns True if the formula's predicate arities and sort constraints
    are satisfied recursively.
    """
    if isinstance(formula, Atomic):
        if len(formula.terms) != formula.predicate.arity:
            return False
        for term, expected_sort in zip(formula.terms, formula.predicate.arg_sorts):
            if isinstance(term, Variable) and term.sort != expected_sort:
                # Sort mismatch — BEING is a universal supersort
                if expected_sort != Sort.BEING and term.sort != Sort.BEING:
                    return False
            elif isinstance(term, Constant) and term.sort != expected_sort:
                if expected_sort != Sort.BEING and term.sort != Sort.BEING:
                    return False
        return True
    elif isinstance(formula, Equals):
        return True  # equality is always well-formed at syntax level
    elif isinstance(formula, Not):
        return is_well_formed(formula.operand)
    elif isinstance(formula, (And, Or)):
        return is_well_formed(formula.left) and is_well_formed(formula.right)
    elif isinstance(formula, Implies):
        return is_well_formed(formula.antecedent) and is_well_formed(formula.consequent)
    elif isinstance(formula, (ForAll, Exists)):
        return is_well_formed(formula.body)
    return False


def free_variables(formula: Formula) -> set[Variable]:
    """Collect all free (unbound) variables in a formula."""
    if isinstance(formula, Atomic):
        result: set[Variable] = set()
        for t in formula.terms:
            result |= _term_variables(t)
        return result
    elif isinstance(formula, Equals):
        return _term_variables(formula.left) | _term_variables(formula.right)
    elif isinstance(formula, Not):
        return free_variables(formula.operand)
    elif isinstance(formula, (And, Or)):
        return free_variables(formula.left) | free_variables(formula.right)
    elif isinstance(formula, Implies):
        return free_variables(formula.antecedent) | free_variables(formula.consequent)
    elif isinstance(formula, (ForAll, Exists)):
        return free_variables(formula.body) - {formula.variable}
    return set()


def _term_variables(term: Term) -> set[Variable]:
    """Collect all variables in a term."""
    if isinstance(term, Variable):
        return {term}
    elif isinstance(term, Constant):
        return set()
    elif isinstance(term, FunctionApplication):
        result: set[Variable] = set()
        for arg in term.args:
            result |= _term_variables(arg)
        return result
    return set()


def formula_depth(formula: Formula) -> int:
    """Compute the nesting depth of a formula."""
    if isinstance(formula, (Atomic, Equals)):
        return 1
    elif isinstance(formula, Not):
        return 1 + formula_depth(formula.operand)
    elif isinstance(formula, (And, Or)):
        return 1 + max(formula_depth(formula.left), formula_depth(formula.right))
    elif isinstance(formula, Implies):
        return 1 + max(
            formula_depth(formula.antecedent),
            formula_depth(formula.consequent)
        )
    elif isinstance(formula, (ForAll, Exists)):
        return 1 + formula_depth(formula.body)
    return 0
