"""Music DNA Builder.

This module defines :class:`MusicDNABuilder`, which transforms
extracted analyzer features into a normalized, versioned
:class:`~music_dna.music_dna.MusicDNA` object.

The builder is deterministic: running it twice on the same input
produces identical output.  It validates all features against the
:class:`~features.registry.FeatureRegistry`, normalizes values via
:class:`~music_dna.normalizer.Normalizer`, and produces an immutable
MusicDNA.
"""

from __future__ import annotations

from types import MappingProxyType
from typing import Protocol

from features.definition import DataType, FeatureDefinition
from features.registry import FeatureRegistry
from music_dna.exceptions import (
    DuplicateFeatureError,
    InvalidValueError,
    MissingFeatureError,
    UnknownFeatureError,
)
from music_dna.music_dna import MusicDNA, MusicDNAMetadata
from music_dna.normalizer import Normalizer


class _TrackFeatureLike(Protocol):
    """Protocol for objects that look like TrackFeature ORM rows."""

    name: str
    value: str | None


class _AnalyzerRunLike(Protocol):
    """Protocol for objects that look like AnalyzerRun ORM rows."""

    analyzer_name: str
    analyzer_version: str


class MusicDNABuilder:
    """Builds immutable :class:`MusicDNA` objects from analyzer features.

    The builder validates that every registered feature is present in
    the input, that no unknown or duplicate features exist, and that
    all values are valid.  It then normalizes each value and creates
    an immutable MusicDNA.

    Attributes:
        feature_registry: The registry of feature definitions.
        normalizer: The normalizer instance to use.
    """

    BUILDER_VERSION = "1.0.0"
    SCHEMA_VERSION = "1.0"

    def __init__(
        self,
        feature_registry: FeatureRegistry,
        normalizer: Normalizer | None = None,
    ) -> None:
        """Initialize the builder.

        Args:
            feature_registry: A fully configured FeatureRegistry.
            normalizer: Optional Normalizer instance.  If ``None``,
                a default :class:`Normalizer` is created.
        """
        self.feature_registry = feature_registry
        self.normalizer = normalizer or Normalizer()

    def build(
        self,
        track_id: str,
        features: list[_TrackFeatureLike],
        created_at: str,
    ) -> MusicDNA:
        """Build a MusicDNA from extracted features.

        Args:
            track_id: UUID of the track.
            features: List of TrackFeature-like objects with ``name``
                and ``value`` attributes.
            created_at: ISO 8601 UTC timestamp string.  Provided by
                the caller to ensure determinism.

        Returns:
            An immutable :class:`MusicDNA` instance.

        Raises:
            DuplicateFeatureError: If the same feature name appears
                twice in the input.
            UnknownFeatureError: If a feature name is not in the
                FeatureRegistry.
            MissingFeatureError: If a registered feature is missing
                from the input.
            InvalidValueError: If a feature value is None.
        """
        raw_values: dict[str, str] = {}
        for tf in features:
            if tf.name in raw_values:
                raise DuplicateFeatureError(
                    f"Duplicate feature identifier: '{tf.name}'"
                )
            raw_values[tf.name] = tf.value

        for name in raw_values:
            if not self.feature_registry.contains(name):
                raise UnknownFeatureError(
                    f"Feature '{name}' is not in the FeatureRegistry"
                )

        registered_ids = {d.identifier for d in self.feature_registry.list()}
        missing = registered_ids - set(raw_values.keys())
        if missing:
            sorted_missing = sorted(missing)
            raise MissingFeatureError(
                f"Missing {len(sorted_missing)} required features: "
                f"{', '.join(sorted_missing)}"
            )

        normalized: dict[str, float | str | int | bool] = {}
        source_versions: dict[str, str] = {}

        for tf in features:
            definition = self.feature_registry.get(tf.name)

            if tf.value is None:
                raise InvalidValueError(f"Feature '{tf.name}' has a None value")

            deserialized = self._deserialize_value(tf.value, definition)
            normalized_value = self.normalizer.normalize(
                deserialized,
                definition.normalization,
            )
            normalized[tf.name] = normalized_value

            run = getattr(tf, "analyzer_run", None)
            if run is not None:
                source_versions[run.analyzer_name] = run.analyzer_version

        metadata = MusicDNAMetadata(
            builder_version=self.BUILDER_VERSION,
            feature_count=len(normalized),
            normalization_version=self.normalizer.NORMALIZATION_VERSION,
            source_analyzer_versions=MappingProxyType(source_versions),
        )

        return MusicDNA(
            track_id=track_id,
            schema_version=self.SCHEMA_VERSION,
            created_at=created_at,
            values=MappingProxyType(normalized),
            metadata=metadata,
        )

    @staticmethod
    def _deserialize_value(
        raw: str,
        definition: FeatureDefinition,
    ) -> float | str | int | bool:
        """Deserialize a string value to the type specified by the definition.

        Args:
            raw: The raw string value from the database.
            definition: The feature definition specifying the data type.

        Returns:
            The deserialized value.

        Raises:
            InvalidValueError: If the value cannot be deserialized
                to the expected type.
        """
        if definition.data_type == DataType.FLOAT:
            try:
                return float(raw)
            except ValueError:
                raise InvalidValueError(
                    f"Feature '{definition.identifier}' value '{raw}' "
                    "cannot be converted to float"
                ) from None
        elif definition.data_type == DataType.INT:
            try:
                return int(raw)
            except ValueError:
                raise InvalidValueError(
                    f"Feature '{definition.identifier}' value '{raw}' "
                    "cannot be converted to int"
                ) from None
        elif definition.data_type == DataType.BOOL:
            if raw.lower() in ("true", "1", "yes"):
                return True
            if raw.lower() in ("false", "0", "no"):
                return False
            raise InvalidValueError(
                f"Feature '{definition.identifier}' value '{raw}' "
                "cannot be converted to bool"
            )
        else:
            return raw
