"""Component types: the atomic building blocks of OHRs.

Paradigm-independent — ``Component.value`` accepts any hashable type.
"""

from __future__ import annotations

from dataclasses import dataclass, replace
from enum import StrEnum
from typing import Any

from flexohr.harmony.enums import (
    EMPTY_PROPS,
    ComponentStatus,
    FrozenProperties,
    KeyValue,
    _property_key,
)


def _coerce_props(raw: Any) -> FrozenProperties:
    """Coerce *raw* to ``FrozenProperties`` (tuple of PropertyItems).

    Accepts:
    - tuple: returned as-is
    - frozenset of (key, value) pairs: converted to tuple of KeyValue/enum items (backward compat)
    - dict of {key: value}: converted to tuple of KeyValue/enum items (backward compat)
    - list/iterable: converted to tuple
    """
    if isinstance(raw, tuple):
        return raw
    if isinstance(raw, frozenset):
        # Backward compat: frozenset of (key, value) pairs -> tuple of items
        result = []
        for item in raw:
            if isinstance(item, tuple) and len(item) == 2:
                key, value = item
                if isinstance(value, StrEnum):
                    result.append(value)
                else:
                    result.append(KeyValue(key=str(key), value=value))
            else:
                result.append(item)
        return tuple(result)
    if isinstance(raw, dict):
        result = []
        for key, value in raw.items():
            if isinstance(value, StrEnum):
                result.append(value)
            else:
                result.append(KeyValue(key=str(key), value=value))
        return tuple(result)
    return tuple(raw)


def _props_repr(properties: FrozenProperties) -> str:
    """Concise text representation of properties, grouping values by key.

    For example, ``(ToneFunction.third, ToneFunction.bass)`` becomes
    ``"tf=third, bass"`` rather than ``"tf=third, tf=bass"``.
    """
    from collections import OrderedDict

    grouped: OrderedDict[str, list[str]] = OrderedDict()
    for item in properties:
        try:
            k = _property_key(item)
        except TypeError:
            k = "?"
        if isinstance(item, KeyValue):
            v_display = repr(item.value)
        elif hasattr(item, "name"):
            v_display = item.name
        else:
            v_display = repr(item)
        # Abbreviate common keys
        abbr = _PROP_KEY_ABBREV.get(k, k)
        grouped.setdefault(abbr, []).append(v_display)

    parts = []
    for k, vals in grouped.items():
        joined = ", ".join(vals)
        parts.append(f"{k}={joined}")
    return ", ".join(parts)


_PROP_KEY_ABBREV: dict[str, str] = {
    "tone_function": "tf",
    "chord_quality": "quality",
    "collection_type": "coll",
    "component_status": "status",
}


def _props_badges_html(
    properties: FrozenProperties,
    bg: str = "#e5e7eb",
    bg_map: dict[str, str] | None = None,
) -> str:
    """Render properties as HTML badges, grouping values under repeated keys.

    For example, ``(ToneFunction.fifth, ToneFunction.bass)`` renders as a
    single badge ``tone_function=fifth, bass`` instead of two separate badges.

    Parameters
    ----------
    properties : FrozenProperties
        The property items to render.
    bg : str
        Default background colour for badges.
    bg_map : dict[str, str] or None
        Optional mapping from property key to background colour.
    """
    from collections import OrderedDict

    if bg_map is None:
        bg_map = {}

    # Group values by key, preserving insertion order
    grouped: OrderedDict[str, list[str]] = OrderedDict()
    for item in properties:
        try:
            k = _property_key(item)
        except TypeError:
            k = "?"
        if isinstance(item, KeyValue):
            v_display = repr(item.value)
        elif hasattr(item, "name"):
            v_display = item.name
        else:
            v_display = repr(item)
        grouped.setdefault(k, []).append(v_display)

    parts = []
    for k, vals in grouped.items():
        colour = bg_map.get(k, bg)
        joined = ", ".join(vals)
        parts.append(
            f'<span style="font-family:monospace;background:{colour};border-radius:3px;'
            f'padding:0 4px;font-size:0.85em;margin-left:2px">{k}={joined}</span>'
        )
    return "".join(parts)


@dataclass(frozen=True, slots=True)
class Component:
    """Immutable wrapper around any value with arbitrary properties.

    Paradigm-independent: *value* can be any hashable type (pitchspace
    Point/Vector, frequency bin, rhythmic pattern, etc.).
    """

    value: Any = None
    properties: FrozenProperties = EMPTY_PROPS
    status: ComponentStatus = ComponentStatus.present

    def __post_init__(self) -> None:
        if not isinstance(self.properties, tuple):
            object.__setattr__(self, "properties", _coerce_props(self.properties))

    def __repr__(self) -> str:
        """Concise representation grouping properties by key.

        Examples::

            Component(SPC(C))
            Component(SIC(M3), tf=third, bass)
            Component(None, tone='root', octave=4)
        """
        parts = [repr(self.value)]
        if self.properties:
            parts.append(", ")
            parts.append(_props_repr(self.properties))
        if self.status is not ComponentStatus.present:
            parts.append(f" [{self.status.value}]")
        return f"C({' '.join(''.join(parts).split())})"

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

    @property
    def is_absent(self) -> bool:
        """True if this is an :class:`AbsentComponent`."""
        return isinstance(self, AbsentComponent)

    @property
    def is_undefined(self) -> bool:
        """True if this is an :class:`UndefinedComponent`."""
        return isinstance(self, UndefinedComponent)

    def get_property(self, key: str, default: Any = None) -> Any:
        """Look up properties by *key*.

        Returns a single value when exactly one match exists, a tuple when
        multiple matches exist, or *default* when no match is found.

        For enum members, the key is the snake_case enum class name
        (e.g., ``"tone_function"`` for ``ToneFunction`` members).
        For KeyValue items, the key is the explicit ``.key`` attribute.
        For dataclass objects, the key is the snake_case class name.
        """
        matches = []
        for item in self.properties:
            try:
                if _property_key(item) == key:
                    if isinstance(item, KeyValue):
                        matches.append(item.value)
                    else:
                        matches.append(item)
            except TypeError:
                continue
        if not matches:
            return default
        if len(matches) == 1:
            return matches[0]
        return tuple(matches)

    def to_dict(self) -> dict[str, Any]:
        """Serialize to a verbose, self-describing dict."""
        from flexohr.core.ohr import _component_to_dict

        return _component_to_dict(self)

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> Component:
        """Deserialize from dict."""
        from flexohr.core.ohr import _component_from_dict

        return _component_from_dict(d)

    def with_(self, **kwargs: Any) -> Component:
        """Return a new Component with specified fields replaced."""
        if "properties" in kwargs:
            p = kwargs["properties"]
            if isinstance(p, dict):
                kwargs["properties"] = _coerce_props(p)
            elif isinstance(p, frozenset):
                kwargs["properties"] = _coerce_props(p)
        return replace(self, **kwargs)

    def with_properties(self, *items: Any, **new_props: Any) -> Component:
        """Return a new Component with additional properties appended.

        Accepts positional items (enum members, dataclass objects) and
        keyword arguments (plain key-value pairs).
        """
        from flexohr.harmony.enums import props as _props

        extra = _props(*items, **new_props)
        return replace(self, properties=self.properties + extra)

    def _repr_html_(self) -> str:
        # Value display
        val = self.value
        if hasattr(val, "_repr_html_"):
            val_html = val._repr_html_()
        else:
            val_html = f'<span style="font-family:monospace">{repr(val)}</span>'

        # Properties badges (grouped by key)
        props_html = _props_badges_html(self.properties, bg="#e5e7eb")

        # Status indicator (only show if not present)
        status_html = ""
        if self.status.value != "present":
            status_html = (
                f'<span style="font-family:monospace;background:#fecaca;border-radius:3px;'
                f'padding:0 4px;font-size:0.85em;margin-left:2px">{self.status.value}</span>'
            )

        return f"{val_html}{props_html}{status_html}"


@dataclass(frozen=True, slots=True)
class AbsentComponent(Component):
    """A component that is structurally expected but explicitly not present.

    Use as ``reference_component`` to signal "no reference" (distinct from
    ``None`` which means "inherit from parent").
    """

    reason: str = ""

    def __post_init__(self) -> None:
        Component.__post_init__(self)
        if self.status is ComponentStatus.present:
            object.__setattr__(self, "status", ComponentStatus.absent)

    def __repr__(self) -> str:
        reason_part = f", reason={self.reason!r}" if self.reason else ""
        return f"AbsentComponent({repr(self.value)}{reason_part})"

    def _repr_html_(self) -> str:
        val = self.value
        if hasattr(val, "_repr_html_"):
            val_html = val._repr_html_()
        else:
            val_html = f'<span style="font-family:monospace">{repr(val)}</span>'

        reason_html = f' <span style="font-style:italic;color:#888">({self.reason})</span>' if self.reason else ""
        return (
            f'<span style="font-family:monospace;color:#dc2626;text-decoration:line-through">'
            f"{val_html}</span>"
            f'<span style="font-family:monospace;background:#fecaca;border-radius:3px;'
            f'padding:0 4px;font-size:0.85em;margin-left:2px">{self.status.value}</span>'
            f"{reason_html}"
        )


@dataclass(frozen=True, slots=True)
class UndefinedComponent(Component):
    """Placeholder component — blocks reference inheritance.

    When used as ``reference_component`` on an OHR, prevents inheriting
    the reference from parent OHRs.  Value is always ``None``.
    """

    def __repr__(self) -> str:
        return "UndefinedComponent()"

    def __post_init__(self) -> None:
        Component.__post_init__(self)
        if self.value is not None:
            object.__setattr__(self, "value", None)


# Module-level sentinel
UNDEFINED = UndefinedComponent()
