"""Voice design resource for generating speech from voice descriptions."""

from __future__ import annotations

import base64
from collections.abc import AsyncIterator, Iterator
from contextlib import asynccontextmanager, contextmanager
from typing import TYPE_CHECKING

from murmr._errors import MurmrError
from murmr._streaming import (
    async_stream,
    sync_stream,
)
from murmr._types import (
    AudioStreamChunk,
    ExtractEmbeddingsResponse,
    VoiceDeleteResponse,
    VoiceListResponse,
    VoiceSaveResponse,
)
from murmr._validate import validate_id, validate_input

if TYPE_CHECKING:
    from murmr._client import AsyncMurmrClient, MurmrClient


class AsyncVoices:
    """Async resource for voice design endpoints."""

    def __init__(self, client: AsyncMurmrClient) -> None:
        self._client = client

    async def design(
        self,
        *,
        input: str,
        voice_description: str,
        language: str = "English",
    ) -> bytes:
        """Generate speech with a natural language voice description.

        Returns a complete WAV buffer (24kHz, mono, 16-bit PCM).
        """
        validate_input(input)
        if not voice_description or not voice_description.strip():
            raise MurmrError("voice_description is required")

        body = {
            "input": input,
            "voice_description": voice_description,
            "language": language,
        }

        response = await self._client.request(
            "POST", "/v1/voices/design", json=body
        )
        return response.content

    @asynccontextmanager
    async def design_stream(
        self,
        *,
        input: str,
        voice_description: str,
        language: str = "English",
    ) -> AsyncIterator[AsyncIterator[AudioStreamChunk]]:
        """Stream speech with a voice description. Yields AudioStreamChunk objects."""
        validate_input(input)
        if not voice_description or not voice_description.strip():
            raise MurmrError("voice_description is required")

        body = {
            "input": input,
            "voice_description": voice_description,
            "language": language,
        }

        response = await self._client.request(
            "POST", "/v1/voices/design/stream", json=body, stream=True
        )
        async with async_stream(response) as chunks:
            yield chunks

    async def list_voices(self) -> VoiceListResponse:
        """List saved voices for the authenticated user."""
        response = await self._client.request("GET", "/v1/voices")
        return VoiceListResponse.model_validate(response.json())

    async def save(
        self,
        *,
        name: str,
        audio: bytes,
        description: str,
        ref_text: str,
        language: str = "English",
    ) -> VoiceSaveResponse:
        """Save a voice from audio for reuse with saved-voice endpoints.

        Args:
            name: Display name for the voice (1-50 chars).
            audio: WAV audio bytes from a VoiceDesign generation.
            description: The original voice description.
            ref_text: Transcript of the reference audio (required for ICL extraction).
            language: Language code (default: "English").
        """
        if not name or not name.strip():
            raise MurmrError("name is required")
        if len(name.strip()) > 50:
            raise MurmrError("name must be 50 characters or fewer")
        if not description or not description.strip():
            raise MurmrError("description is required")
        if not audio:
            raise MurmrError("audio is required")
        if not ref_text or not ref_text.strip():
            raise MurmrError("ref_text is required (transcript of the reference audio)")

        audio_b64 = base64.b64encode(audio).decode("ascii")
        response = await self._client.request(
            "POST",
            "/v1/voices",
            json={
                "name": name,
                "audio": audio_b64,
                "description": description,
                "language": language,
                "ref_text": ref_text,
            },
        )
        return VoiceSaveResponse.model_validate(response.json())

    async def delete(self, voice_id: str) -> VoiceDeleteResponse:
        """Delete a saved voice by ID.

        Args:
            voice_id: The voice ID (e.g., "voice_abc123def456").
        """
        validate_id(voice_id, "voice_id")
        response = await self._client.request("DELETE", f"/v1/voices/{voice_id}")
        return VoiceDeleteResponse.model_validate(response.json())

    async def extract_embeddings(
        self,
        *,
        audio: bytes,
        ref_text: str,
    ) -> ExtractEmbeddingsResponse:
        """Extract voice embeddings from audio for portable voice cloning.

        Args:
            audio: WAV audio bytes (from a VoiceDesign generation or recording).
            ref_text: Transcript of the reference audio.
        """
        if not audio:
            raise MurmrError("audio is required")
        if not ref_text or not ref_text.strip():
            raise MurmrError("ref_text is required (transcript of the reference audio)")

        audio_b64 = base64.b64encode(audio).decode("ascii")
        response = await self._client.request(
            "POST",
            "/v1/voices/extract-embeddings",
            json={"audio": audio_b64, "ref_text": ref_text},
        )
        result = response.json()

        if not result.get("prompt_data"):
            raise MurmrError(result.get("error", "Failed to extract embeddings"))

        return ExtractEmbeddingsResponse(
            prompt_data=result["prompt_data"],
            prompt_size_bytes=result.get("prompt_size_bytes", 0),
        )


class SyncVoices:
    """Sync resource for voice design endpoints."""

    def __init__(self, client: MurmrClient) -> None:
        self._client = client

    def design(
        self,
        *,
        input: str,
        voice_description: str,
        language: str = "English",
    ) -> bytes:
        """Generate speech with a natural language voice description.

        Returns a complete WAV buffer (24kHz, mono, 16-bit PCM).
        """
        validate_input(input)
        if not voice_description or not voice_description.strip():
            raise MurmrError("voice_description is required")

        body = {
            "input": input,
            "voice_description": voice_description,
            "language": language,
        }

        response = self._client.request(
            "POST", "/v1/voices/design", json=body
        )
        return response.content

    @contextmanager
    def design_stream(
        self,
        *,
        input: str,
        voice_description: str,
        language: str = "English",
    ) -> Iterator[Iterator[AudioStreamChunk]]:
        """Stream speech with a voice description. Yields AudioStreamChunk objects."""
        validate_input(input)
        if not voice_description or not voice_description.strip():
            raise MurmrError("voice_description is required")

        body = {
            "input": input,
            "voice_description": voice_description,
            "language": language,
        }

        response = self._client.request(
            "POST", "/v1/voices/design/stream", json=body, stream=True
        )
        with sync_stream(response) as chunks:
            yield chunks

    def list_voices(self) -> VoiceListResponse:
        """List saved voices for the authenticated user."""
        response = self._client.request("GET", "/v1/voices")
        return VoiceListResponse.model_validate(response.json())

    def save(
        self,
        *,
        name: str,
        audio: bytes,
        description: str,
        ref_text: str,
        language: str = "English",
    ) -> VoiceSaveResponse:
        """Save a voice from audio for reuse with saved-voice endpoints.

        Args:
            name: Display name for the voice (1-50 chars).
            audio: WAV audio bytes from a VoiceDesign generation.
            description: The original voice description.
            ref_text: Transcript of the reference audio (required for ICL extraction).
            language: Language code (default: "English").
        """
        if not name or not name.strip():
            raise MurmrError("name is required")
        if len(name.strip()) > 50:
            raise MurmrError("name must be 50 characters or fewer")
        if not description or not description.strip():
            raise MurmrError("description is required")
        if not audio:
            raise MurmrError("audio is required")
        if not ref_text or not ref_text.strip():
            raise MurmrError("ref_text is required (transcript of the reference audio)")

        audio_b64 = base64.b64encode(audio).decode("ascii")
        response = self._client.request(
            "POST",
            "/v1/voices",
            json={
                "name": name,
                "audio": audio_b64,
                "description": description,
                "language": language,
                "ref_text": ref_text,
            },
        )
        return VoiceSaveResponse.model_validate(response.json())

    def delete(self, voice_id: str) -> VoiceDeleteResponse:
        """Delete a saved voice by ID.

        Args:
            voice_id: The voice ID (e.g., "voice_abc123def456").
        """
        validate_id(voice_id, "voice_id")
        response = self._client.request("DELETE", f"/v1/voices/{voice_id}")
        return VoiceDeleteResponse.model_validate(response.json())

    def extract_embeddings(
        self,
        *,
        audio: bytes,
        ref_text: str,
    ) -> ExtractEmbeddingsResponse:
        """Extract voice embeddings from audio for portable voice cloning.

        Args:
            audio: WAV audio bytes (from a VoiceDesign generation or recording).
            ref_text: Transcript of the reference audio.
        """
        if not audio:
            raise MurmrError("audio is required")
        if not ref_text or not ref_text.strip():
            raise MurmrError("ref_text is required (transcript of the reference audio)")

        audio_b64 = base64.b64encode(audio).decode("ascii")
        response = self._client.request(
            "POST",
            "/v1/voices/extract-embeddings",
            json={"audio": audio_b64, "ref_text": ref_text},
        )
        result = response.json()

        if not result.get("prompt_data"):
            raise MurmrError(result.get("error", "Failed to extract embeddings"))

        return ExtractEmbeddingsResponse(
            prompt_data=result["prompt_data"],
            prompt_size_bytes=result.get("prompt_size_bytes", 0),
        )
