"""Detail-level map and resolution for UKO domains.

``DetailLevelMap`` maps named detail levels to integer depths, supports
parent-chain inheritance, cycle detection, and immutable level storage
via ``MappingProxyType``.

## DetailDepth

At the universal level (UKO Layer 0), detail depth is a **non-negative
integer** -- 0 is the most minimal representation and each increment
reveals more.  Each UKO domain extension registers a ``DetailLevelMap``
that maps named labels to specific integer depths.  Maps are inherited:
a child map includes all entries from its parent map.

Based on ``docs/specification.md`` ACMS / CRP sections and ADR-014.
"""

from __future__ import annotations

import copy
import threading
from collections.abc import Mapping
from types import MappingProxyType
from typing import Any

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    PrivateAttr,
    field_serializer,
    field_validator,
    model_validator,
)

# ---------------------------------------------------------------------------
# Public type alias
# ---------------------------------------------------------------------------

#: Detail depth can be a raw non-negative integer **or** a named level
#: string (e.g., ``"SIGNATURES"``) resolved via the active
#: ``DetailLevelMap``.
DetailDepth = int | str

# ---------------------------------------------------------------------------
# Domain-specific exceptions
# ---------------------------------------------------------------------------


class DetailLevelError(ValueError):
    """Raised when a detail level name cannot be resolved."""


class DetailLevelCycleError(DetailLevelError):
    """Raised when a circular parent chain is detected."""


# ---------------------------------------------------------------------------
# DetailLevelMap
# ---------------------------------------------------------------------------


class DetailLevelMap(BaseModel):
    """Maps named detail levels to integer depths for a UKO domain.

    Inherited: a child map includes all entries from its parent map,
    and may insert additional levels at any integer position.

    The ``levels`` field is stored as a ``MappingProxyType`` to prevent
    accidental mutation of module-level constant maps.  Use
    ``register()`` to add levels after construction (creates a new
    immutable snapshot each time).

    Example::

        code_map = DetailLevelMap(
            domain="uko-code:",
            parent=None,
            levels={"MODULE_LISTING": 0, "FULL_SOURCE": 9},
            max_depth=9,
        )
        assert code_map.resolve(4) == 4
        assert code_map.resolve("MODULE_LISTING") == 0
    """

    domain: str = Field(
        ...,
        min_length=1,
        description="UKO namespace (e.g., 'uko-code:', 'uko-py:')",
    )
    parent: DetailLevelMap | None = Field(
        default=None,
        description="Parent map to inherit from",
    )
    levels: Mapping[str, int] = Field(
        default_factory=dict,
        description=(
            "Named level -> integer depth.  "
            "Accepts a ``dict`` on construction but is stored at runtime "
            "as a ``MappingProxyType`` (read-only view) by the "
            "``_freeze_levels`` model validator."
        ),
    )
    max_depth: int = Field(
        ...,
        ge=0,
        description="Maximum meaningful depth for this domain",
    )

    @field_validator("levels", mode="before")
    @classmethod
    def validate_levels(
        cls: type[DetailLevelMap],
        v: Mapping[str, int],
    ) -> dict[str, int]:
        """Validate non-negative depths and normalise to a plain dict.

        Accepts any ``Mapping`` (``dict``, ``MappingProxyType``, etc.)
        on construction and during ``validate_assignment`` round-trips.
        Always returns a plain ``dict`` for the model validator to freeze.
        """
        plain = dict(v)
        for name, depth in plain.items():
            if depth < 0:
                raise ValueError(f"Detail level '{name}' has negative depth {depth}")
        return plain

    @model_validator(mode="after")
    def _freeze_levels(self) -> DetailLevelMap:
        """Replace the mutable levels dict with a read-only proxy.

        Runs on initial construction and on every ``validate_assignment``
        cycle (Pydantic 2.12+ re-runs model validators on assignment).
        The ``__setattr__`` override provides an additional
        defense-in-depth safety net.
        """
        object.__setattr__(self, "levels", MappingProxyType(dict(self.levels)))
        return self

    def __setattr__(self, name: str, value: Any) -> None:
        """Re-freeze ``levels`` on assignment as a defensive measure.

        Pydantic 2.12+ re-runs both field *and* model validators on
        ``validate_assignment``, so the ``_freeze_levels`` model
        validator already handles the normal case.  This override is
        kept as defense-in-depth in case the Pydantic behavior changes
        in a future release.  It re-freezes from the validated
        ``self.levels`` (not the raw *value*) so any normalisation
        applied by the field validator is preserved.
        """
        super().__setattr__(name, value)
        if (
            name == "levels"
            and isinstance(self.levels, (dict, Mapping))
            and not isinstance(self.levels, MappingProxyType)
        ):
            object.__setattr__(self, "levels", MappingProxyType(dict(self.levels)))

    def __copy__(self) -> DetailLevelMap:
        """Support ``copy.copy()`` despite unpicklable MappingProxyType.

        Constructs a new instance directly (not via ``model_copy``)
        to avoid infinite recursion: ``model_copy(deep=False)``
        internally calls ``__copy__``.
        """
        return DetailLevelMap(
            domain=self.domain,
            parent=self.parent,
            levels=dict(self.levels),
            max_depth=self.max_depth,
        )

    def __deepcopy__(self, memo: dict[int, Any] | None = None) -> DetailLevelMap:
        """Support ``copy.deepcopy()`` despite unpicklable fields.

        Constructs a new instance directly (not via ``model_copy``)
        to avoid infinite recursion.  The ``_register_lock``
        (a ``PrivateAttr``) gets a fresh instance automatically.
        """
        if memo is None:
            memo = {}
        return DetailLevelMap(
            domain=self.domain,
            parent=copy.deepcopy(self.parent, memo),
            levels=copy.deepcopy(dict(self.levels), memo),
            max_depth=self.max_depth,
        )

    @field_serializer("levels")
    @classmethod
    def _serialize_levels(
        cls: type[DetailLevelMap],
        v: Mapping[str, int],
    ) -> dict[str, int]:
        """Serialize MappingProxyType back to a plain dict for JSON."""
        return dict(v)

    def resolve(self, depth: DetailDepth) -> int:
        """Resolve a named level or integer to an integer depth.

        Args:
            depth: Either a raw integer (clamped to ``max_depth``) or
                a named level string looked up in this map, then in
                parent maps.

        Returns:
            Resolved integer depth.

        Raises:
            ValueError: If *depth* is a string not found in this map
                or any ancestor, or if a cycle is detected in the
                parent chain.
        """
        if isinstance(depth, int):
            return max(0, min(depth, self.max_depth))
        visited: set[int] = set()
        current: DetailLevelMap | None = self
        while current is not None:
            map_id = id(current)
            if map_id in visited:
                raise DetailLevelCycleError(
                    f"Cycle detected in DetailLevelMap parent chain "
                    f"while resolving '{depth}'"
                )
            visited.add(map_id)
            if depth in current.levels:
                return current.levels[depth]
            current = current.parent
        raise DetailLevelError(f"Unknown detail level: {depth}")

    #: Per-instance lock for ``register()`` thread safety.
    _register_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)

    def register(self, name: str, value: int) -> None:
        """Register a custom detail level.

        Creates a new immutable snapshot of the levels mapping.
        Thread-safe via an internal lock.

        Args:
            name: Named level label.
            value: Integer depth to map to.

        Raises:
            ValueError: If *value* is negative or exceeds ``max_depth``.
        """
        if value < 0:
            raise ValueError(f"Detail level '{name}' has negative depth {value}")
        if value > self.max_depth:
            raise ValueError(
                f"Detail level '{name}' depth {value} exceeds "
                f"max_depth {self.max_depth}"
            )
        with self._register_lock:
            updated = dict(self.levels)
            updated[name] = value
            object.__setattr__(self, "levels", MappingProxyType(updated))

    def effective_levels(
        self, *, _visited: set[int] | None = None
    ) -> MappingProxyType[str, int]:
        """Return the full merged level map including inherited entries.

        Returns an immutable ``MappingProxyType`` so callers cannot
        accidentally mutate inherited level data.  Parent entries are
        included first; child entries override any collisions.

        Raises:
            DetailLevelCycleError: If a circular parent chain is
                detected.
        """
        if _visited is None:
            _visited = set()
        map_id = id(self)
        if map_id in _visited:
            raise DetailLevelCycleError(
                "Cycle detected in DetailLevelMap parent chain "
                "during effective_levels()"
            )
        _visited.add(map_id)
        merged: dict[str, int] = (
            dict(self.parent.effective_levels(_visited=_visited))
            if self.parent is not None
            else {}
        )
        merged.update(self.levels)
        return MappingProxyType(merged)

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        arbitrary_types_allowed=True,
    )
