"""Core type definitions: FancyStrEnum, CodecRegistry, FrozenProperties.

All types here are paradigm-independent — no pitch-specific concepts.
"""

from __future__ import annotations

import dataclasses
from dataclasses import dataclass, field
from enum import EnumMeta, StrEnum, auto
from typing import Any, ClassVar, Self, Union

from flexohr.errors import CodecRegistryError

# ── PropertyItem & FrozenProperties ──


@dataclass(frozen=True, slots=True)
class KeyValue:
    """A plain key-value property item for non-enum, non-dataclass properties."""

    key: str
    value: Any

    def to_json(self) -> dict[str, Any]:
        """Serialize to JSON-compatible dict."""
        return {"key": self.key, "value": self.value}


# PropertyItem: an enum member, a frozen dataclass, or a KeyValue pair.
# Enum members imply key = snake_case(enum_class_name), value = member.
# Dataclass objects imply key = snake_case(class_name), value = the object.
PropertyItem = Union["FancyStrEnum", KeyValue, Any]

FrozenProperties = tuple[PropertyItem, ...]
"""Immutable, ordered property bag. Items are enum members, dataclass objects, or KeyValue pairs."""

EMPTY_PROPS: FrozenProperties = ()


def _property_key(item: PropertyItem) -> str:
    """Derive the implicit key from a property item.

    - FancyStrEnum member: snake_case of the enum class name
      (e.g., ToneFunction.root -> "tone_function")
    - KeyValue: the explicit .key attribute
    - Frozen dataclass: snake_case of the class name
    """
    if isinstance(item, KeyValue):
        return item.key
    if isinstance(item, StrEnum):
        # e.g. ToneFunction -> "tone_function"
        cls_name = type(item).__name__
        return _camel_to_snake(cls_name)
    if dataclasses.is_dataclass(item) and not isinstance(item, type):
        cls_name = type(item).__name__
        return _camel_to_snake(cls_name)
    raise TypeError(f"Cannot derive property key from {type(item).__name__}: {item!r}")


def _camel_to_snake(name: str) -> str:
    """Convert CamelCase to snake_case."""
    import re

    s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
    return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()


def props(*items: PropertyItem, **kwargs: Any) -> FrozenProperties:
    """Convenience constructor for FrozenProperties.

    Accepts positional arguments (enum members, dataclass objects) and
    keyword arguments (plain key-value pairs wrapped as KeyValue).

    Examples::

        props(ToneFunction.root, ChordQuality.major_triad)
        props(ToneFunction.root, numeral="V")
        props(chord_quality=ChordQuality.major_triad)  # backward compat
    """
    result: list[PropertyItem] = list(items)
    for key, value in kwargs.items():
        if isinstance(value, StrEnum):
            # If the value is an enum member, store it directly
            result.append(value)
        else:
            result.append(KeyValue(key=key, value=value))
    return tuple(result)


# ── CodecRegistry ──


@dataclass
class CodecRegistry:
    """Bidirectional mapping between enum members and annotation-standard strings.

    Each annotation standard (e.g. "dcml", "romantext") gets its own
    {member_value <-> standard_string} mapping.
    """

    _encoders: dict[str, dict[str, str]] = field(default_factory=dict)
    _decoders: dict[str, dict[str, str]] = field(default_factory=dict)
    _enum_cls: type | None = field(default=None, repr=False)

    def register(self, standard: str, mapping: dict[str, str]) -> None:
        """Register *mapping* ``{member_value: standard_string}`` for *standard*."""
        enc: dict[str, str] = {}
        dec: dict[str, str] = {}
        for member_value, standard_string in mapping.items():
            enc[member_value] = standard_string
            dec[standard_string] = member_value
        self._encoders[standard] = enc
        self._decoders[standard] = dec

    def encode(self, member: StrEnum, standard: str) -> str:
        """Enum member -> standard string."""
        try:
            return self._encoders[standard][member.value]
        except KeyError:
            raise CodecRegistryError(f"No mapping for {member!r} in standard {standard!r}")

    def decode(self, value: str, standard: str) -> str:
        """Standard string -> enum member value."""
        try:
            return self._decoders[standard][value]
        except KeyError:
            raise CodecRegistryError(f"No mapping for {value!r} in standard {standard!r}")


# ── FancyStrEnum ──


class _FancyStrEnumMeta(EnumMeta):
    """Metaclass that auto-creates a CodecRegistry for each FancyStrEnum subclass."""

    def __new__(
        mcs,
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        **kwargs: Any,
    ) -> _FancyStrEnumMeta:
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
        if name != "FancyStrEnum":
            cls._codec_registry = CodecRegistry(_enum_cls=cls)  # type: ignore[attr-defined]
        return cls  # type: ignore[return-value]


class FancyStrEnum(StrEnum, metaclass=_FancyStrEnumMeta):
    """StrEnum with ``auto()`` lowercase names, alias support, and codec registry.

    - ``auto()`` generates lowercase member names (Python 3.11+ StrEnum default).
    - Aliases: assign the same string value as an existing member.
    - Format conversion: ``member.to_format(standard)`` / ``Cls.from_format(value, standard)``.
    - Alias string lookup: ``ChordQuality("o7")`` resolves via name-based fallback.
    """

    _codec_registry: ClassVar[CodecRegistry]

    @classmethod
    def _missing_(cls, value: str) -> FancyStrEnum | None:
        """Fall back to name-based lookup for alias names."""
        try:
            return cls[value]
        except KeyError:
            return None

    def to_format(self, standard: str) -> str:
        """Map this member to *standard*'s string representation."""
        return self._codec_registry.encode(self, standard)

    @classmethod
    def from_format(cls, value: str, standard: str) -> Self:
        """Parse *value* from *standard* into an enum member."""
        member_value = cls._codec_registry.decode(value, standard)
        return cls(member_value)

    @classmethod
    def register_standard(cls, name: str, mapping: dict[str, str]) -> None:
        """Register an annotation standard's bidirectional mapping."""
        cls._codec_registry.register(name, mapping)

    @property
    def short_name(self) -> str:
        """Return the shortest alias name for this member.

        If multiple aliases have the same length, returns the first one
        encountered in ``__members__``. If no alias is shorter than the
        canonical name, returns the canonical name.

        Examples::

            >>> CollectionType.major.short_name
            'M'
            >>> CollectionType.dorian.short_name
            'dor'
        """
        shortest = self.name
        for alias_name, member in type(self).__members__.items():
            if member is self and len(alias_name) < len(shortest):
                shortest = alias_name
        return shortest


# ── Concrete enums ──


# ── Type registries for lossless serialization ──


_VALUE_TYPE_REGISTRY: dict[str, type] = {}
"""Maps short type names (e.g. "spc", "si") to their classes for deserialization."""


def register_value_type(name: str, cls: type) -> None:
    """Register a value type for serialization round-trips.

    Paradigm modules call this at import time so the core serializer
    can reconstruct typed values without importing paradigm code directly.
    """
    _VALUE_TYPE_REGISTRY[name] = cls


def get_value_type(name: str) -> type | None:
    """Look up a registered value type by name."""
    return _VALUE_TYPE_REGISTRY.get(name)


def get_value_type_name(value: Any) -> str | None:
    """Look up the registered name for a value's type, or None if unregistered."""
    val_type = type(value)
    for name, cls in _VALUE_TYPE_REGISTRY.items():
        if cls is val_type:
            return name
    return None


_ENUM_REGISTRY: dict[str, type] = {}
"""Maps enum class names (e.g. "ToneFunction") to their classes for deserialization."""


def register_enum_type(name: str, cls: type) -> None:
    """Register an enum type for property deserialization."""
    _ENUM_REGISTRY[name] = cls


def get_enum_type(name: str) -> type | None:
    """Look up a registered enum type by class name."""
    return _ENUM_REGISTRY.get(name)


class ComponentStatus(FancyStrEnum):
    """Status of a component within an OHR."""

    present = auto()
    absent = auto()
    added = auto()
    implied = auto()
    replaced = auto()
    suspended = auto()
    # Aliases
    pr = "present"
    ab = "absent"
    ad = "added"
    im = "implied"
    rp = "replaced"
    su = "suspended"


class Source(FancyStrEnum):
    """Filter for given vs. inferred data on accessor methods."""

    any = auto()
    given = auto()
    inferred = auto()
    # Aliases
    a = "any"
    g = "given"
    i = "inferred"


# Register core enums for property serialization round-trips
register_enum_type("ComponentStatus", ComponentStatus)
register_enum_type("Source", Source)
