"""Core types and constants for arioso.

Defines the unified output types (Song, AudioResult) and the canonical
set of 40 affordance names that all platform adapters map to.
"""

from typing import Optional, Any
from dataclasses import dataclass, field


@dataclass
class AudioResult:
    """Unified audio output from any music generation platform.

    Exactly one of audio_bytes, audio_url, or audio_array will typically
    be populated by the platform adapter. Conversion between forms can
    be done on demand by the caller.

    Attributes:
        audio_bytes: Raw audio file bytes (MP3, WAV, etc.).
        audio_url: URL to download the audio file.
        audio_array: Numpy array of audio samples.
        sample_rate: Sample rate in Hz (e.g. 44100, 32000).
        format: Audio format string (e.g. "mp3", "wav", "pcm").
        duration_seconds: Duration of the audio in seconds.
    """

    audio_bytes: Optional[bytes] = field(default=None, repr=False)
    audio_url: str = ""
    audio_array: Optional[Any] = field(default=None, repr=False)
    sample_rate: int = 0
    format: str = ""
    duration_seconds: float = 0.0


@dataclass
class Song:
    """A generated song with its metadata and audio data.

    Attributes:
        id: Provider-specific identifier for the song.
        title: Song title (may be auto-generated by the platform).
        status: Generation status: pending, generating, complete, error.
        audio: The audio output.
        metadata: Provider-specific metadata dict.
        platform: Name of the platform that generated this song.
    """

    id: str = ""
    title: str = ""
    status: str = "pending"
    audio: AudioResult = field(default_factory=AudioResult)
    metadata: dict = field(default_factory=dict)
    platform: str = ""

    @property
    def is_error(self) -> bool:
        """Check if this Song represents an error response."""
        if self.status == "error":
            return True
        meta_code = self.metadata.get("code") if self.metadata else None
        if meta_code and meta_code != 200:
            return True
        return False

    @property
    def error_message(self) -> str:
        """Extract error message from metadata, if any."""
        if self.metadata:
            msg = self.metadata.get("msg", "")
            if msg:
                code = self.metadata.get("code", "")
                return f"(code {code}): {msg}" if code else msg
        if self.status == "error":
            return "Unknown error"
        return ""

    @property
    def audio_bytes(self) -> Optional[bytes]:
        """Shortcut to audio.audio_bytes."""
        return self.audio.audio_bytes

    @property
    def audio_url(self) -> str:
        """Shortcut to audio.audio_url."""
        return self.audio.audio_url

    @property
    def audio_array(self) -> Optional[Any]:
        """Shortcut to audio.audio_array."""
        return self.audio.audio_array

    @property
    def sample_rate(self) -> int:
        """Shortcut to audio.sample_rate."""
        return self.audio.sample_rate


# ---------------------------------------------------------------------------
# Affordances: the unified parameter vocabulary
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class Affordance:
    """Descriptor for a unified parameter affordance.

    Attributes:
        name: Canonical parameter name used at the facade level.
        type: Expected Python type.
        description: Human-readable description.
        default: Default value (None means no default / required).
    """

    name: str
    type: type
    description: str
    default: Any = None


AFFORDANCES: dict[str, Affordance] = {
    "prompt": Affordance("prompt", str, "Text description of desired music"),
    "negative_prompt": Affordance("negative_prompt", str, "Elements to avoid"),
    "lyrics": Affordance("lyrics", str, "Custom lyrics text"),
    "duration": Affordance("duration", float, "Output length in seconds"),
    "bpm": Affordance("bpm", int, "Beats per minute"),
    "key": Affordance("key", str, 'Musical key (e.g. "C major")'),
    "genre": Affordance("genre", str, "Genre tag or category"),
    "energy": Affordance("energy", float, "Energy/intensity level 0-1"),
    "brightness": Affordance("brightness", float, "Spectral brightness 0-1"),
    "instruments": Affordance("instruments", list, "Instrument selection"),
    "instrumental": Affordance("instrumental", bool, "Force instrumental-only output"),
    "title": Affordance("title", str, "Song title"),
    "structure": Affordance("structure", list, "Song section structure"),
    "audio_input": Affordance("audio_input", bytes, "Reference audio for conditioning"),
    "audio_input_strength": Affordance(
        "audio_input_strength", float, "How much reference audio influences output 0-1"
    ),
    "continue_from": Affordance("continue_from", str, "ID/URL of track to extend from"),
    "continue_at": Affordance(
        "continue_at", float, "Timestamp in seconds to continue from"
    ),
    "mask_start": Affordance("mask_start", float, "Inpainting region start in seconds"),
    "mask_end": Affordance("mask_end", float, "Inpainting region end in seconds"),
    "model": Affordance("model", str, "Model version or variant"),
    "seed": Affordance("seed", int, "Random seed for reproducibility"),
    "guidance": Affordance("guidance", float, "Classifier-free guidance scale"),
    "temperature": Affordance("temperature", float, "Sampling randomness"),
    "top_k": Affordance("top_k", int, "Top-k sampling parameter"),
    "top_p": Affordance("top_p", float, "Top-p (nucleus) sampling"),
    "num_steps": Affordance("num_steps", int, "Number of inference/diffusion steps"),
    "sampler": Affordance("sampler", str, "Sampling algorithm for diffusion"),
    "batch_size": Affordance("batch_size", int, "Number of outputs per request"),
    "output_format": Affordance("output_format", str, "Desired output format"),
    "bitrate": Affordance("bitrate", int, "Audio bitrate in kbps"),
    "prompt_weight": Affordance("prompt_weight", float, "Strength of prompt adherence"),
    "interpolation_alpha": Affordance(
        "interpolation_alpha", float, "Blend between two prompts 0-1"
    ),
    "wait_for_completion": Affordance(
        "wait_for_completion", bool, "Block until audio is ready"
    ),
    "voice_id": Affordance("voice_id", str, "AI singer/voice model selection"),
    "pitch_shift": Affordance("pitch_shift", int, "Pitch adjustment in semitones"),
    "watermark": Affordance("watermark", bool, "Apply content authenticity watermark"),
    "extend_stride": Affordance(
        "extend_stride", float, "Overlap stride for extended generation"
    ),
    "max_tokens": Affordance("max_tokens", int, "Max generated tokens"),
    "repetition_penalty": Affordance(
        "repetition_penalty", float, "Penalize repeated patterns"
    ),
    "looping": Affordance("looping", bool, "Generate loopable audio"),
}
