"""Config flow for llama.cpp integration."""

import logging
from typing import Any, cast, override

import openai
import voluptuous as vol

from homeassistant.config_entries import (
    ConfigEntry,
    ConfigEntryState,
    ConfigFlow,
    ConfigFlowResult,
    ConfigSubentryFlow,
    SubentryFlowResult,
)
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_PROMPT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import llm
from homeassistant.helpers.selector import (
    NumberSelector,
    NumberSelectorConfig,
    SelectOptionDict,
    SelectSelector,
    SelectSelectorConfig,
    SelectSelectorMode,
    TemplateSelector,
)

from .api import (
    async_create_client,
    async_list_models,
    async_validate_completions,
    model_name_to_title,
    recommended_model,
)
from .const import (
    CONF_BASE_URL,
    CONF_CHAT_MODEL,
    CONF_MAX_TOKENS,
    CONF_RECOMMENDED,
    CONF_STREAMING,
    CONF_TEMPERATURE,
    CONF_TOP_P,
    DEFAULT_BASE_URL,
    DOMAIN,
    LOGGER,
    RECOMMENDED_MAX_TOKENS,
    RECOMMENDED_TEMPERATURE,
    RECOMMENDED_TOP_P,
)

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_BASE_URL, default=DEFAULT_BASE_URL): str,
        vol.Optional(CONF_API_KEY): str,
    }
)


class LlamaCppConfigFlow(ConfigFlow, domain=DOMAIN):
    """Handle a config flow for llama.cpp."""

    VERSION = 1

    data: dict[str, Any] | None = None
    client: openai.AsyncOpenAI | None = None
    models: list[str] | None = None

    @override
    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle the initial step."""
        errors = {}
        if user_input is not None:
            self._async_abort_entries_match(user_input)
            try:
                self.client = await async_create_client(self.hass, user_input)
                self.models = await async_list_models(self.client)
            except HomeAssistantError as err:
                LOGGER.error("Connection validation failed: %s", err)
                errors["base"] = err.translation_key or "unknown"
            except Exception:  # pylint: disable=broad-except # noqa: BLE001
                LOGGER.exception("Unexpected exception")
                errors["base"] = "unknown"
            else:
                self.data = user_input
                return await self.async_step_model()

        return self.async_show_form(
            step_id="user",
            data_schema=STEP_USER_DATA_SCHEMA,
            errors=errors,
        )

    async def async_step_model(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle selecting a model."""
        assert self.client is not None
        assert self.models is not None
        assert self.data is not None
        errors = {}
        if user_input is not None:
            model = user_input[CONF_CHAT_MODEL]
            try:
                await async_validate_completions(
                    self.client,
                    model=model,
                    stream=False,
                )
            except HomeAssistantError as err:
                LOGGER.error("Model completion validation failed: %s", err)
                errors["base"] = err.translation_key or "unknown"
            else:
                stream_support = True
                try:
                    await async_validate_completions(
                        self.client,
                        model=model,
                        stream=True,
                    )
                except HomeAssistantError:
                    stream_support = False

                base_options = {
                    **user_input,
                }
                return self.async_create_entry(
                    title=self.data[CONF_BASE_URL],
                    data={
                        **self.data,
                        CONF_STREAMING: stream_support,
                    },
                    subentries=[
                        {
                            "subentry_type": "conversation",
                            "data": {
                                CONF_RECOMMENDED: True,
                                CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
                                **base_options,
                            },
                            "title": model_name_to_title(model),
                            "unique_id": None,
                        },
                    ],
                )

        return self.async_show_form(
            step_id="model",
            data_schema=self.add_suggested_values_to_schema(
                vol.Schema(
                    {
                        vol.Optional(
                            CONF_CHAT_MODEL,
                        ): SelectSelector(
                            SelectSelectorConfig(
                                options=self.models,
                                translation_key=CONF_CHAT_MODEL,
                                mode=SelectSelectorMode.DROPDOWN,
                                custom_value=True,
                            ),
                        ),
                    }
                ),
                {
                    CONF_CHAT_MODEL: (user_input or {}).get(
                        CONF_CHAT_MODEL, recommended_model(self.models)
                    ),
                },
            ),
            errors=errors,
        )

    @classmethod
    @callback
    @override
    def async_get_supported_subentry_types(
        cls, config_entry: ConfigEntry
    ) -> dict[str, type[ConfigSubentryFlow]]:
        """Return subentries supported by this integration."""
        return {
            "conversation": ConversationSubentryFlowHandler,
        }


class ConversationSubentryFlowHandler(ConfigSubentryFlow):
    """Flow for managing conversation subentries."""

    last_rendered_recommended = False
    options: dict[str, Any] | None = None
    models: list[str] | None = None

    @property
    def _openai_client(self) -> openai.AsyncOpenAI:
        """Return the OpenAI client."""
        return cast(openai.AsyncOpenAI, self._get_entry().runtime_data)

    async def _get_models(self) -> list[str] | None:
        """Return the list of models."""
        if self.models is None:
            self.models = await async_list_models(self._openai_client)
        return self.models

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Add a subentry."""
        if self._get_entry().state is not ConfigEntryState.LOADED:
            return self.async_abort(reason="entry_not_loaded")

        try:
            models = await self._get_models()
        except HomeAssistantError:
            return self.async_abort(reason="cannot_connect")
        self.options = {
            CONF_RECOMMENDED: True,
            CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
            CONF_CHAT_MODEL: recommended_model(models),
        }
        self.last_rendered_recommended = cast(
            bool, self.options.get(CONF_RECOMMENDED, False)
        )
        return await self.async_step_init()

    async def async_step_reconfigure(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Handle reconfiguration of a subentry."""
        return await self.async_step_init()

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Manage initial options."""
        # abort if entry is not loaded
        if self._get_entry().state is not ConfigEntryState.LOADED:
            return self.async_abort(reason="entry_not_loaded")

        if self.options is None:
            self.options = self._get_reconfigure_subentry().data.copy()
            self.last_rendered_recommended = cast(
                bool, self.options.get(CONF_RECOMMENDED, False)
            )

        try:
            models = await self._get_models()
        except HomeAssistantError:
            return self.async_abort(reason="cannot_connect")

        options = self.options

        if user_input is not None:
            model = user_input[CONF_CHAT_MODEL]
            try:
                await async_validate_completions(
                    self._openai_client,
                    model=model,
                    stream=self._get_entry().data.get(CONF_STREAMING, False),
                )
            except HomeAssistantError as err:
                LOGGER.error("Model completion validation failed: %s", err)
                return self.async_show_form(
                    step_id="init",
                    data_schema=self.add_suggested_values_to_schema(
                        vol.Schema(
                            llama_cpp_config_option_schema(self.hass, options, models)
                        ),
                        user_input,
                    ),
                    errors={"base": err.translation_key or "unknown"},
                )

            if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
                if self.source == "user":
                    return self.async_create_entry(
                        title=model_name_to_title(user_input[CONF_CHAT_MODEL]),
                        data=user_input,
                    )
                return self.async_update_and_abort(
                    self._get_entry(),
                    self._get_reconfigure_subentry(),
                    data=user_input,
                    title=model_name_to_title(user_input[CONF_CHAT_MODEL]),
                )

            self.last_rendered_recommended = user_input[CONF_RECOMMENDED]

            options = {
                CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
                CONF_PROMPT: user_input[CONF_PROMPT],
                CONF_CHAT_MODEL: user_input[CONF_CHAT_MODEL],
                CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API, []),
            }

        schema = llama_cpp_config_option_schema(self.hass, options, models)
        return self.async_show_form(
            step_id="init",
            data_schema=self.add_suggested_values_to_schema(
                vol.Schema(schema), options
            ),
        )


def llama_cpp_config_option_schema(
    hass: HomeAssistant,
    options: dict[str, Any],
    models: list[str] | None = None,
) -> dict:
    """Return a schema for llama.cpp completion options."""
    hass_apis: list[SelectOptionDict] = [
        SelectOptionDict(
            label=api.name,
            value=api.id,
        )
        for api in llm.async_get_apis(hass)
    ]
    LOGGER.debug("Available LLM APIs: %s", hass_apis)

    schema: dict[vol.Required | vol.Optional, Any] = {}

    schema.update(
        {
            vol.Optional(
                CONF_PROMPT,
                description={
                    "suggested_value": options.get(
                        CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT
                    )
                },
            ): TemplateSelector(),
            vol.Optional(
                CONF_LLM_HASS_API,
            ): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
        }
    )
    schema.update(
        {
            vol.Optional(
                CONF_CHAT_MODEL,
                description={"suggested_value": options.get(CONF_CHAT_MODEL)},
                default=options.get(CONF_CHAT_MODEL, recommended_model(models)),
            ): SelectSelector(
                SelectSelectorConfig(
                    options=models or [],
                    translation_key=CONF_CHAT_MODEL,
                    mode=SelectSelectorMode.DROPDOWN,
                    custom_value=True,
                ),
            ),
            vol.Required(
                CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
            ): bool,
        }
    )

    if options.get(CONF_RECOMMENDED):
        return schema

    schema.update(
        {
            vol.Optional(
                CONF_MAX_TOKENS,
                description={"suggested_value": options.get(CONF_MAX_TOKENS)},
                default=RECOMMENDED_MAX_TOKENS,
            ): int,
            vol.Optional(
                CONF_TOP_P,
                description={"suggested_value": options.get(CONF_TOP_P)},
                default=RECOMMENDED_TOP_P,
            ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
            vol.Optional(
                CONF_TEMPERATURE,
                description={"suggested_value": options.get(CONF_TEMPERATURE)},
                default=RECOMMENDED_TEMPERATURE,
            ): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
        }
    )
    return schema
