"""TNFR Grammar: Types and Exceptions

Enums, exception classes, and validation result types for TNFR grammar.

Terminology (TNFR semantics):
- "node" == resonant locus (structural coherence site); kept for NetworkX compatibility
- Future semantic aliasing ("locus") must preserve public API stability
"""

from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING, Any, Mapping, Sequence

from ..constants.canonical import U6_STRUCTURAL_POTENTIAL_LIMIT

if TYPE_CHECKING:
    from ..types import Glyph, NodeId, TNFRGraph
else:
    NodeId = Any
    TNFRGraph = Any
    from ..types import Glyph

from ..config.physics_derivation import (
    derive_bifurcation_handlers_from_physics as _derive_handlers,
)
from ..config.physics_derivation import (
    derive_bifurcation_triggers_from_physics as _derive_triggers,
)
from ..config.physics_derivation import (
    derive_destabilizers_from_physics as _derive_destabilizers,
)
from ..config.physics_derivation import (
    derive_end_operators_from_physics as _derive_ends,
)
from ..config.physics_derivation import (
    derive_stabilizers_from_physics as _derive_stabilizers,
)
from ..config.physics_derivation import (
    derive_start_operators_from_physics as _derive_starts,
)
from ..config.physics_derivation import (
    derive_transformers_from_physics as _derive_transformers,
)
from ..validation.base import ValidationOutcome

# ============================================================================
# Operator Sets (Shared Contract Predicates)
# ============================================================================
#
# SINGLE SOURCE OF TRUTH.  These sets are DERIVED from the per-operator
# contract predicates in ``tnfr.config.physics_derivation`` — they are not
# hand-maintained frozensets. Every sequence-grammar consumer (grammar_core,
# grammar_dynamics, grammar_patterns, and runtime preconditions) must import
# from here. Canonical U6 consumes field snapshots and does not define another
# operator-role set. The derivation rationale (why each operator is a generator /
# closure / stabilizer / destabilizer / transformer) lives in the predicate
# docstrings of physics_derivation. These are operator-role policies motivated
# by ∂EPI/∂t = νf·ΔNFR, not trajectory theorems.


# U1a: Generators - Create EPI from null/dormant states (AL, NAV, REMESH)
GENERATORS = _derive_starts()

# U1b: Registered operational closures (SHA, NAV, REMESH, OZ). Membership does
# not by itself establish that the terminal state is an attractor.
CLOSURES = _derive_ends()

# U2: Stabilizer coverage roles (IL directly reduces pressure; THOL reorganizes
# a bifurcation while preserving global form): IL, THOL
STABILIZERS = _derive_stabilizers()

# U2: Operators that incur destabilizer debt through pressure, phase, or
# capacity stress: OZ, ZHIR, VAL
DESTABILIZERS = _derive_destabilizers()

# U3: Coupling/Resonance - Require phase verification
COUPLING_RESONANCE = frozenset({"coupling", "resonance"})

# U4a: Trigger roles requiring handler coverage (OZ, ZHIR)
BIFURCATION_TRIGGERS = _derive_triggers()

# U4a: Declared handler coverage (THOL, IL), not measured threshold evidence
BIFURCATION_HANDLERS = _derive_handlers()

# U4b: Transformer roles requiring preceding context (ZHIR, THOL)
TRANSFORMERS = _derive_transformers()

# U5: Multi-Scale Coherence - Recursive generators and scale stabilizers
RECURSIVE_GENERATORS = frozenset({"recursivity"})
SCALE_STABILIZERS = STABILIZERS


class StructuralPattern(Enum):
    """Classification labels for TNFR operator sequences.

    Two distinct axes are mixed in this legacy enum. The CANONICAL grammar
    typology is the structural-shape axis — exactly five types from TNFR.pdf
    §2.3 "Tabla comparativa de estructuras glíficas" (LINEAR, BIFURCATED,
    FRACTAL, CYCLIC, HIERARCHICAL), mirrored by
    :class:`tnfr.operators.grammar_canon.StructuralType`. The remaining members
    are NON-canonical heuristic application labels (domain, learning process,
    operational meta) kept for backward compatibility; they do not emerge from
    the nodal dynamics and are not part of the canonical grammar typology.

    For canonical structural classification use ``grammar_canon.StructuralType``
    and ``CANONICAL_STRUCTURAL_TYPES`` below; ``grammar_canon`` also provides
    the mapping from any label here to its canonical structural type.
    """

    # --- CANONICAL structural typology (TNFR.pdf "Tabla comparativa") ---------
    BIFURCATED = "bifurcated"  # Bifurcada — OZ → [ZHIR | NUL] (union)
    LINEAR = "linear"  # Lineal — concatenation
    HIERARCHICAL = "hierarchical"  # Jerárquica — nested THOL[...] (Dyck)
    FRACTAL = "fractal"  # Fractal — self-similar repeat (star)
    CYCLIC = "cyclic"  # Cíclica — close-and-reopen feedback cycle

    # --- LEGACY heuristic labels (NON-canonical application metadata) ---------
    # Application domain axis (not a structural shape):
    THERAPEUTIC = "therapeutic"
    EDUCATIONAL = "educational"
    ORGANIZATIONAL = "organizational"
    CREATIVE = "creative"
    REGENERATIVE = "regenerative"
    # Operational meta axis (reduce to a canonical type via grammar_canon):
    COMPLEX = "complex"
    COMPRESS = "compress"
    EXPLORE = "explore"
    RESONATE = "resonate"
    BOOTSTRAP = "bootstrap"
    STABILIZE = "stabilize"
    # Learning-process axis (not a structural shape):
    BASIC_LEARNING = "basic_learning"
    DEEP_LEARNING = "deep_learning"
    EXPLORATORY_LEARNING = "exploratory_learning"
    CONSOLIDATION_CYCLE = "consolidation_cycle"
    ADAPTIVE_MUTATION = "adaptive_mutation"
    UNKNOWN = "unknown"


#: The five canonical structural types (TNFR.pdf "Tabla comparativa"), the only
#: members of :class:`StructuralPattern` that constitute the canonical grammar
#: typology. Mirrored by :class:`grammar_canon.StructuralType`.
CANONICAL_STRUCTURAL_TYPES = frozenset(
    {
        StructuralPattern.LINEAR,
        StructuralPattern.BIFURCATED,
        StructuralPattern.FRACTAL,
        StructuralPattern.CYCLIC,
        StructuralPattern.HIERARCHICAL,
    }
)

# ============================================================================
# Glyph-Function Name Mappings
# ============================================================================

# Mapping from Glyph to canonical lowercase executable identifier
GLYPH_TO_FUNCTION = {
    Glyph.AL: "emission",
    Glyph.EN: "reception",
    Glyph.IL: "coherence",
    Glyph.OZ: "dissonance",
    Glyph.UM: "coupling",
    Glyph.RA: "resonance",
    Glyph.SHA: "silence",
    Glyph.VAL: "expansion",
    Glyph.NUL: "contraction",
    Glyph.THOL: "self_organization",
    Glyph.ZHIR: "mutation",
    Glyph.NAV: "transition",
    Glyph.REMESH: "recursivity",
}

# Reverse mapping from canonical lowercase executable identifier to Glyph
FUNCTION_TO_GLYPH = {v: k for k, v in GLYPH_TO_FUNCTION.items()}


def _operator_name(operator: Any) -> Any:
    """Read an operator instance's declared identifier without inventing one.

    An explicit canonical_name remains authoritative. Ordinary name attributes
    retain their case-insensitive compatibility; malformed or missing values
    remain available for the caller's syntax rejection.
    """
    name = getattr(operator, "name", None)
    return getattr(
        operator, "canonical_name", name.lower() if isinstance(name, str) else name
    )


def glyph_function_name(
    val: Any,
    *,
    default: Any = None,
) -> Any:
    """Convert a supported name or glyph to the executable identifier.

    Parameters
    ----------
    val : Glyph | str | None
        Glyph enum, glyph string value ('IL', 'OZ'), or operator name to convert
    default : str | None, optional
        Default value if conversion fails

    Returns
    -------
    str | None
        Canonical lowercase executable identifier or default

    Notes
    -----
    Glyph enum inherits from str, so we must check for Enum type
    BEFORE checking isinstance(val, str), otherwise Glyph instances
    will be returned unchanged instead of being converted.

    The function handles three input types:
    1. Glyph enum (e.g., Glyph.IL) → executable identifier (e.g., 'coherence')
    2. Glyph string value (e.g., 'IL') → executable identifier
    3. Executable identifier (including direct case variants) → canonical form
    """
    if val is None:
        return default
    # Prefer strict Glyph check BEFORE str (Glyph inherits from str)
    if isinstance(val, Glyph):
        return GLYPH_TO_FUNCTION.get(val, default)
    if isinstance(val, str):
        # Check if it's a glyph string value ('IL', 'OZ', etc)
        # Build reverse lookup on first use
        if not hasattr(glyph_function_name, "_glyph_value_map"):
            glyph_function_name._glyph_value_map = {
                g.value: func for g, func in GLYPH_TO_FUNCTION.items()
            }
        # Serialized histories may contain Glyph.VAL or lowercase executable
        # identifiers; direct case variants normalize through the same map.
        token = val.strip()
        if token.startswith("Glyph."):
            token = token.rsplit(".", 1)[-1]
        func_name = glyph_function_name._glyph_value_map.get(token.upper())
        if func_name:
            return func_name
        if token.lower() in FUNCTION_TO_GLYPH:
            return token.lower()
        # Otherwise preserve the unknown token for the caller to reject.
        return val
    # Unknown type: cannot map safely
    return default


def function_name_to_glyph(
    val: Any,
    *,
    default: Any = None,
) -> Any:
    """Convert function name to glyph.

    Parameters
    ----------
    val : str | Glyph | None
        Function name or glyph to convert
    default : Glyph | None, optional
        Default value if conversion fails

    Returns
    -------
    Glyph | None
        Glyph or default
    """
    if val is None:
        return default
    if isinstance(val, Glyph):
        return val
    return FUNCTION_TO_GLYPH.get(val, default)


__all__ = [
    # Validation result types
    "SequenceValidationResult",
    "StructuralPattern",
    # Error classes
    "StructuralGrammarError",
    "StructuralPotentialConfinementError",
    "RepeatWindowError",
    "MutationPreconditionError",
    "TholClosureError",
    "TransitionCompatibilityError",
    "SequenceSyntaxError",
    "GrammarConfigurationError",
    # Glyph mappings
    "GLYPH_TO_FUNCTION",
    "FUNCTION_TO_GLYPH",
    "glyph_function_name",
    "function_name_to_glyph",
    # Operator sets
    "GENERATORS",
    "CLOSURES",
    "STABILIZERS",
    "DESTABILIZERS",
    "COUPLING_RESONANCE",
    "BIFURCATION_TRIGGERS",
    "BIFURCATION_HANDLERS",
    "TRANSFORMERS",
    "RECURSIVE_GENERATORS",
    "SCALE_STABILIZERS",
]

# ============================================================================
# Grammar Errors
# ============================================================================


class StructuralGrammarError(RuntimeError):
    """Base class for structural grammar violations.

    Attributes
    ----------
    rule : str
        Grammar rule that was violated
    candidate : str
        Operator/glyph that caused violation
    message : str
        Error description
    window : int | None
        Grammar window if applicable
    threshold : float | None
        Threshold value if applicable
    order : Sequence[str] | None
        Operator sequence if applicable
    context : dict
        Additional context information
    """

    def __init__(
        self,
        *,
        rule: str,
        candidate: str,
        message: str,
        window: int | None = None,
        threshold: float | None = None,
        order: list[str] | None = None,
        context: dict[str, Any] | None = None,
    ):
        self.rule = rule
        self.candidate = candidate
        self.message = message
        self.window = window
        self.threshold = threshold
        self.order = order
        self.context = context or {}
        super().__init__(message)

    def attach_context(self, **context: Any) -> "StructuralGrammarError":
        """Attach additional context to error.

        Parameters
        ----------
        **context : Any
            Additional context key-value pairs

        Returns
        -------
        StructuralGrammarError
            Self for chaining
        """
        self.context.update(context)
        return self

    def to_payload(self) -> dict[str, Any]:
        """Convert error to dictionary payload.

        Returns
        -------
        dict
            Error information as dictionary
        """
        return {
            "rule": self.rule,
            "candidate": self.candidate,
            "message": self.message,
            "window": self.window,
            "threshold": self.threshold,
            "order": self.order,
            "context": self.context,
        }


class RepeatWindowError(StructuralGrammarError):
    """Error for repeated operator within window."""


class MutationPreconditionError(StructuralGrammarError):
    """Error for mutation without proper preconditions."""


class TholClosureError(StructuralGrammarError):
    """Error for THOL without proper closure."""


class TransitionCompatibilityError(StructuralGrammarError):
    """Error for incompatible transition."""


class StructuralPotentialConfinementError(StructuralGrammarError):
    """Error for structural potential drift exceeding the selected U6 policy.

    Raised when the supplied before/after fields have mean absolute drift
    Δ Φ_s ≥ π/2. This is a telemetry alert, not proof of fragmentation.
    """

    def __init__(
        self,
        delta_phi_s: float,
        threshold: float = U6_STRUCTURAL_POTENTIAL_LIMIT,
        sequence: list[str] | None = None,
    ):
        msg = (
            f"U6 structural-potential drift policy exceeded: "
            f"mean |Δ Φ_s| = {delta_phi_s:.3f} ≥ {threshold:.3f}. "
            f"Inspect the supplied trajectory; this alert alone does not prove "
            f"fragmentation or a grammar-word violation."
        )
        super().__init__(
            rule="U6_CONFINEMENT",
            candidate="sequence",
            message=msg,
            threshold=threshold,
            order=sequence,
            context={"delta_phi_s": delta_phi_s},
        )


class SequenceSyntaxError(ValueError):
    """Error in sequence syntax.

    Attributes
    ----------
    index : int
        Position in sequence where error occurred
    token : object
        Token that caused the error
    message : str
        Error description
    """

    def __init__(self, index: int, token: Any, message: str):
        self.index = index
        self.token = token
        self.message = message
        super().__init__(f"At index {index}, token '{token}': {message}")


class SequenceValidationResult(ValidationOutcome[tuple[str, ...]]):
    """Validation outcome for operator sequences with rich metadata.

    Attributes
    ----------
    tokens : tuple[str, ...]
        Original input tokens (non-canonical)
    canonical_tokens : tuple[str, ...]
        Canonicalized operator names
    message : str
        Human-readable validation message
    metadata : Mapping[str, object]
        Additional validation metadata (detected_pattern, flags, etc.)
    error : SequenceSyntaxError | None
        Syntax error details if validation failed
    """

    __slots__ = ("tokens", "canonical_tokens", "message", "metadata", "error")

    def __init__(
        self,
        *,
        tokens: Sequence[str],
        canonical_tokens: Sequence[str],
        passed: bool,
        message: str,
        metadata: Mapping[str, object] | None = None,
        summary: Mapping[str, object] | None = None,
        artifacts: Mapping[str, object] | None = None,
        error: SequenceSyntaxError | None = None,
    ) -> None:
        tokens_tuple = tuple(tokens)
        canonical_tuple = tuple(canonical_tokens)
        metadata_map = dict(metadata or {})

        summary_map = (
            dict(summary)
            if summary is not None
            else {
                "message": message,
                "tokens": canonical_tuple,
                "metadata": metadata_map,
            }
        )
        if error is not None and "error" not in summary_map:
            summary_map["error"] = {
                "index": error.index,
                "token": error.token,
                "message": error.message,
            }

        artifacts_map = (
            dict(artifacts)
            if artifacts is not None
            else {
                "canonical_tokens": canonical_tuple,
                "tokens": tokens_tuple,
            }
        )

        super().__init__(
            subject=canonical_tuple,
            passed=passed,
            summary=summary_map,
            artifacts=artifacts_map,
        )

        self.tokens = tokens_tuple
        self.canonical_tokens = canonical_tuple
        self.message = message
        self.metadata = metadata_map
        self.error = error


class GrammarConfigurationError(ValueError):
    """Error in grammar configuration.

    Attributes
    ----------
    section : str
        Configuration section with error
    messages : list[str]
        Error messages
    details : list[tuple[str, str]]
        Additional details
    """

    def __init__(
        self,
        section: str,
        messages: list[str],
        *,
        details: list[tuple[str, str]] | None = None,
    ):
        self.section = section
        self.messages = messages
        self.details = details or []
        msg = f"Configuration error in {section}: {'; '.join(messages)}"
        super().__init__(msg)


def record_grammar_violation(
    G,  # TNFRGraph (runtime fallback)
    node,  # NodeId (runtime fallback)
    error: StructuralGrammarError,
    *,
    stage: str,
) -> None:
    """Record grammar violation in node metadata.

    Parameters
    ----------
    G : TNFRGraph
        Graph containing node
    node : NodeId
        Node where violation occurred
    error : StructuralGrammarError
        Grammar error to record
    stage : str
        Processing stage when error occurred
    """
    if "grammar_violations" not in G.nodes[node]:
        G.nodes[node]["grammar_violations"] = []
    G.nodes[node]["grammar_violations"].append(
        {
            "stage": stage,
            "error": error.to_payload(),
        }
    )
