"""Helper classes for Google Assistant SDK integration."""

from dataclasses import dataclass
from http import HTTPStatus
import logging
from typing import Any
import uuid

from aiohttp import web
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
from grpc import RpcError

from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
    ATTR_MEDIA_ANNOUNCE,
    ATTR_MEDIA_CONTENT_ID,
    ATTR_MEDIA_CONTENT_TYPE,
    DOMAIN as MP_DOMAIN,
    SERVICE_PLAY_MEDIA,
    MediaType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
    HomeAssistantError,
    OAuth2TokenRequestReauthError,
    ServiceValidationError,
)
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later

from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES

_LOGGER = logging.getLogger(__name__)

DEFAULT_LANGUAGE_CODES = {
    "de": "de-DE",
    "en": "en-US",
    "es": "es-ES",
    "fr": "fr-FR",
    "it": "it-IT",
    "ja": "ja-JP",
    "ko": "ko-KR",
    "pt": "pt-BR",
}

type GoogleAssistantSDKConfigEntry = ConfigEntry[GoogleAssistantSDKRuntimeData]


@dataclass
class GoogleAssistantSDKRuntimeData:
    """Runtime data for Google Assistant SDK."""

    session: OAuth2Session
    mem_storage: InMemoryStorage


@dataclass
class CommandResponse:
    """Response from a single command to Google Assistant Service."""

    text: str


async def async_send_text_commands(
    hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None
) -> list[CommandResponse]:
    """Send text commands to Google Assistant Service."""
    # There can only be 1 entry (config_flow has single_instance_allowed)
    entries = hass.config_entries.async_loaded_entries(DOMAIN)
    if not entries:
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key="entry_not_loaded",
        )
    entry: GoogleAssistantSDKConfigEntry = entries[0]

    session = entry.runtime_data.session
    try:
        await session.async_ensure_token_valid()
    except OAuth2TokenRequestReauthError:
        entry.async_start_reauth(hass)
        raise

    credentials = Credentials(session.token[CONF_ACCESS_TOKEN])  # type: ignore[no-untyped-call]
    language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
    command_response_list = []
    with TextAssistant(
        credentials, language_code, audio_out=bool(media_players)
    ) as assistant:
        for command in commands:
            try:
                resp = await hass.async_add_executor_job(assistant.assist, command)
            except RpcError as err:
                _LOGGER.error(
                    "Failed to send command '%s' to Google Assistant: %s",
                    command,
                    err,
                )
                raise HomeAssistantError(
                    translation_domain=DOMAIN, translation_key="grpc_error"
                ) from err
            text_response = resp[0]
            _LOGGER.debug("command: %s\nresponse: %s", command, text_response)
            audio_response = resp[2]
            if media_players and audio_response:
                audio_url = GoogleAssistantSDKAudioView.url.format(
                    filename=entry.runtime_data.mem_storage.store_and_get_identifier(
                        audio_response
                    )
                )
                await hass.services.async_call(
                    MP_DOMAIN,
                    SERVICE_PLAY_MEDIA,
                    {
                        ATTR_ENTITY_ID: media_players,
                        ATTR_MEDIA_CONTENT_ID: audio_url,
                        ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
                        ATTR_MEDIA_ANNOUNCE: True,
                    },
                    blocking=True,
                )
            command_response_list.append(CommandResponse(text_response))
    return command_response_list


def default_language_code(hass: HomeAssistant) -> str:
    """Get default language code based on Home Assistant config."""
    language_code = f"{hass.config.language}-{hass.config.country}"
    if language_code in SUPPORTED_LANGUAGE_CODES:
        return language_code
    return DEFAULT_LANGUAGE_CODES.get(hass.config.language, "en-US")


def best_matching_language_code(
    hass: HomeAssistant, assist_language: str, agent_language: str | None = None
) -> str:
    """Get the best matching language.

    Based on the preferred assist language and the configured
    agent language.
    """

    # Use the assist language if supported
    if assist_language in SUPPORTED_LANGUAGE_CODES:
        return assist_language
    language = assist_language.split("-", maxsplit=1)[0]

    # Use the agent language if assist and agent start with the same language part
    if agent_language is not None and agent_language.startswith(language):
        return best_matching_language_code(hass, agent_language)

    # If assist and agent are not matching, try to find the default language
    default_language = DEFAULT_LANGUAGE_CODES.get(language)
    if default_language is not None:
        return default_language

    # If no default agent is available, use the agent language
    if agent_language is not None:
        return best_matching_language_code(hass, agent_language)

    # Fallback to the system default language
    return default_language_code(hass)


class InMemoryStorage:
    """Temporarily store and retrieve data from in memory storage."""

    def __init__(self, hass: HomeAssistant) -> None:
        """Initialize InMemoryStorage."""
        self.hass: HomeAssistant = hass
        self.mem: dict[str, bytes] = {}

    def store_and_get_identifier(self, data: bytes) -> str:
        """Temporarily store data and return identifier to be able to retrieve it.

        Data expires after 5 minutes.
        """
        identifier: str = uuid.uuid1().hex
        self.mem[identifier] = data

        def async_remove_from_mem(*_: Any) -> None:
            """Cleanup memory."""
            self.mem.pop(identifier, None)

        # Remove the entry from memory 5 minutes later
        async_call_later(self.hass, 5 * 60, async_remove_from_mem)

        return identifier

    def retrieve(self, identifier: str) -> bytes | None:
        """Retrieve previously stored data."""
        return self.mem.get(identifier)


class GoogleAssistantSDKAudioView(HomeAssistantView):
    """Google Assistant SDK view to serve audio responses."""

    requires_auth = True
    url = "/api/google_assistant_sdk/audio/{filename}"
    name = "api:google_assistant_sdk:audio"

    def __init__(self, mem_storage: InMemoryStorage) -> None:
        """Initialize GoogleAssistantSDKView."""
        self.mem_storage: InMemoryStorage = mem_storage

    async def get(self, request: web.Request, filename: str) -> web.Response:
        """Start a get request."""
        audio = self.mem_storage.retrieve(filename)
        if not audio:
            return web.Response(status=HTTPStatus.NOT_FOUND)
        return web.Response(body=audio, content_type="audio/mpeg")
