"""Voice resolver for Tea Party — language-aware Polly voice selection.

Queries the Polly DescribeVoices API to select the best available voices
for a given language, with generative > neural > standard engine fallback.
Supports explicit voice overrides that bypass auto-selection entirely.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from alice_cli.errors import AliceCLIError

# Engine preference order: generative is highest quality, then neural, then standard.
_ENGINE_PREFERENCE = ["generative", "neural", "standard"]


@dataclass
class ResolvedVoices:
    """Result of voice resolution for a language."""

    voice_a: str
    voice_b: str
    engine: str  # "generative", "neural", or "standard"
    voice_a_engines: list[str] = field(default_factory=list)
    voice_b_engines: list[str] = field(default_factory=list)
    warning: str | None = None


def resolve_voices(
    session: Any,  # boto3 Session
    lang: str = "en-US",
    voice_a_override: str | None = None,
    voice_b_override: str | None = None,
) -> ResolvedVoices:
    """Select Polly voices for the given language.

    If voice overrides are provided, uses them directly. Otherwise, queries
    Polly DescribeVoices API filtered by language code and selects the best
    available generative voices (preferring one male, one female).

    Falls back to neural, then standard engine if no generative voices
    are available for the language.

    Args:
        session: Authenticated boto3 session.
        lang: Language code (e.g., "en-US", "es-ES", "fr-FR").
        voice_a_override: Explicit voice ID for Host_A (bypasses auto-selection).
        voice_b_override: Explicit voice ID for Host_B (bypasses auto-selection).

    Returns:
        ResolvedVoices with selected voice IDs and engine type.

    Raises:
        AliceCLIError: If the Polly API call fails or no voices are available.
    """
    # Explicit overrides bypass all auto-selection logic.
    if voice_a_override is not None and voice_b_override is not None:
        return ResolvedVoices(
            voice_a=voice_a_override,
            voice_b=voice_b_override,
            engine="generative",
            voice_a_engines=[],
            voice_b_engines=[],
        )

    # Query Polly for available voices in the target language.
    try:
        polly = session.client("polly")
        response = polly.describe_voices(LanguageCode=lang)
    except Exception as exc:
        raise AliceCLIError(f"Failed to query Polly voices for language '{lang}': {exc}") from exc

    all_voices: list[dict[str, Any]] = response.get("Voices", [])

    if not all_voices:
        raise AliceCLIError(f"No Polly voices available for language '{lang}'")

    # Try each engine tier in preference order.
    selected_engine: str | None = None
    candidates: list[dict[str, Any]] = []
    warning: str | None = None

    for engine in _ENGINE_PREFERENCE:
        candidates = [v for v in all_voices if engine in v.get("SupportedEngines", [])]
        if candidates:
            selected_engine = engine
            if engine != "generative":
                warning = (
                    f"No generative voices available for '{lang}'; " f"fell back to {engine} engine"
                )
            break

    if not selected_engine or not candidates:
        raise AliceCLIError(f"No usable Polly voices found for language '{lang}'")

    # Select two distinct voices, preferring gender-diverse pairing.
    # Also prefer voices that support multiple engines for flexibility.
    voice_a_info, voice_b_info = _select_pair(candidates)

    # If only one override is provided, use it and auto-select the other.
    final_a = voice_a_override if voice_a_override is not None else voice_a_info["Id"]
    final_b = voice_b_override if voice_b_override is not None else voice_b_info["Id"]

    # For overridden voices, engines list is empty (unknown).
    voice_a_engines = voice_a_info.get("SupportedEngines", []) if voice_a_override is None else []
    voice_b_engines = voice_b_info.get("SupportedEngines", []) if voice_b_override is None else []

    return ResolvedVoices(
        voice_a=final_a,
        voice_b=final_b,
        engine=selected_engine,
        voice_a_engines=list(voice_a_engines),
        voice_b_engines=list(voice_b_engines),
        warning=warning,
    )


def _select_pair(
    candidates: list[dict[str, Any]],
) -> tuple[dict[str, Any], dict[str, Any]]:
    """Select two distinct voices from candidates, preferring gender diversity.

    Prefers voices that support multiple engines (more flexible downstream).
    If gender-diverse pairing is possible (male + female), selects one of each.
    Otherwise, selects the two best candidates by engine count.

    Args:
        candidates: List of Polly voice dicts with Id, Gender, SupportedEngines.

    Returns:
        Tuple of (voice_a_info, voice_b_info).
    """
    # Sort by number of supported engines (descending) for flexibility preference.
    ranked = sorted(
        candidates,
        key=lambda v: len(v.get("SupportedEngines", [])),
        reverse=True,
    )

    if len(ranked) == 1:
        # Only one voice available — use it for both.
        return ranked[0], ranked[0]

    # Try gender-diverse pairing: pick best male and best female.
    males = [v for v in ranked if v.get("Gender") == "Male"]
    females = [v for v in ranked if v.get("Gender") == "Female"]

    if males and females:
        return males[0], females[0]

    # No gender diversity available — pick top two by engine count.
    return ranked[0], ranked[1]
