"""Config flow to configure deCONZ component."""

from __future__ import annotations

import asyncio
from pprint import pformat
from typing import Any, cast
from urllib.parse import urlparse

import async_timeout
from pydeconz.errors import RequestError, ResponseError
from pydeconz.gateway import DeconzSession
from pydeconz.utils import (
    discovery as deconz_discovery,
    get_bridge_id as deconz_get_bridge_id,
    normalize_bridge_id,
)
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components import ssdp
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client

from .const import (
    CONF_ALLOW_CLIP_SENSOR,
    CONF_ALLOW_DECONZ_GROUPS,
    CONF_ALLOW_NEW_DEVICES,
    CONF_BRIDGE_ID,
    DEFAULT_PORT,
    DOMAIN,
    HASSIO_CONFIGURATION_URL,
    LOGGER,
)
from .gateway import DeconzGateway, get_gateway_from_config_entry

DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de"
CONF_SERIAL = "serial"
CONF_MANUAL_INPUT = "Manually define gateway"


@callback
def get_master_gateway(hass: HomeAssistant) -> DeconzGateway:
    """Return the gateway which is marked as master."""
    for gateway in hass.data[DOMAIN].values():
        if gateway.master:
            return cast(DeconzGateway, gateway)
    raise ValueError


class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
    """Handle a deCONZ config flow."""

    VERSION = 1

    _hassio_discovery: dict[str, Any]

    @staticmethod
    @callback
    def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
        """Get the options flow for this handler."""
        return DeconzOptionsFlowHandler(config_entry)

    def __init__(self) -> None:
        """Initialize the deCONZ config flow."""
        self.bridge_id = ""
        self.bridges: list[dict[str, int | str]] = []
        self.deconz_config: dict[str, int | str] = {}

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle a deCONZ config flow start.

        Let user choose between discovered bridges and manual configuration.
        If no bridge is found allow user to manually input configuration.
        """
        if user_input is not None:

            if CONF_MANUAL_INPUT == user_input[CONF_HOST]:
                return await self.async_step_manual_input()

            for bridge in self.bridges:
                if bridge[CONF_HOST] == user_input[CONF_HOST]:
                    self.bridge_id = cast(str, bridge[CONF_BRIDGE_ID])
                    self.deconz_config = {
                        CONF_HOST: bridge[CONF_HOST],
                        CONF_PORT: bridge[CONF_PORT],
                    }
                    return await self.async_step_link()

        session = aiohttp_client.async_get_clientsession(self.hass)

        try:
            async with async_timeout.timeout(10):
                self.bridges = await deconz_discovery(session)

        except (asyncio.TimeoutError, ResponseError):
            self.bridges = []

        LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges))

        if self.bridges:
            hosts = []

            for bridge in self.bridges:
                hosts.append(bridge[CONF_HOST])

            hosts.append(CONF_MANUAL_INPUT)

            return self.async_show_form(
                step_id="user",
                data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(hosts)}),
            )

        return await self.async_step_manual_input()

    async def async_step_manual_input(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Manual configuration."""
        if user_input:
            self.deconz_config = user_input
            return await self.async_step_link()

        return self.async_show_form(
            step_id="manual_input",
            data_schema=vol.Schema(
                {
                    vol.Required(CONF_HOST): str,
                    vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
                }
            ),
        )

    async def async_step_link(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Attempt to link with the deCONZ bridge."""
        errors: dict[str, str] = {}

        LOGGER.debug(
            "Preparing linking with deCONZ gateway %s", pformat(self.deconz_config)
        )

        if user_input is not None:
            session = aiohttp_client.async_get_clientsession(self.hass)
            deconz_session = DeconzSession(
                session,
                host=self.deconz_config[CONF_HOST],
                port=self.deconz_config[CONF_PORT],
            )

            try:
                async with async_timeout.timeout(10):
                    api_key = await deconz_session.get_api_key()

            except (ResponseError, RequestError, asyncio.TimeoutError):
                errors["base"] = "no_key"

            else:
                self.deconz_config[CONF_API_KEY] = api_key
                return await self._create_entry()

        return self.async_show_form(step_id="link", errors=errors)

    async def _create_entry(self) -> FlowResult:
        """Create entry for gateway."""
        if not self.bridge_id:
            session = aiohttp_client.async_get_clientsession(self.hass)

            try:
                async with async_timeout.timeout(10):
                    self.bridge_id = await deconz_get_bridge_id(
                        session, **self.deconz_config
                    )
                    await self.async_set_unique_id(self.bridge_id)

                    self._abort_if_unique_id_configured(
                        updates={
                            CONF_HOST: self.deconz_config[CONF_HOST],
                            CONF_PORT: self.deconz_config[CONF_PORT],
                            CONF_API_KEY: self.deconz_config[CONF_API_KEY],
                        }
                    )

            except asyncio.TimeoutError:
                return self.async_abort(reason="no_bridges")

        return self.async_create_entry(title=self.bridge_id, data=self.deconz_config)

    async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult:
        """Trigger a reauthentication flow."""
        self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]}

        self.deconz_config = {
            CONF_HOST: config[CONF_HOST],
            CONF_PORT: config[CONF_PORT],
        }

        return await self.async_step_link()

    async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
        """Handle a discovered deCONZ bridge."""
        if (
            discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL)
            != DECONZ_MANUFACTURERURL
        ):
            return self.async_abort(reason="not_deconz_bridge")

        LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info))

        self.bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
        parsed_url = urlparse(discovery_info.ssdp_location)

        entry = await self.async_set_unique_id(self.bridge_id)
        if entry and entry.source == config_entries.SOURCE_HASSIO:
            return self.async_abort(reason="already_configured")

        hostname = cast(str, parsed_url.hostname)
        port = cast(int, parsed_url.port)

        self._abort_if_unique_id_configured(
            updates={CONF_HOST: hostname, CONF_PORT: port}
        )

        self.context.update(
            {
                "title_placeholders": {"host": hostname},
                "configuration_url": f"http://{hostname}:{port}",
            }
        )

        self.deconz_config = {CONF_HOST: hostname, CONF_PORT: port}

        return await self.async_step_link()

    async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult:
        """Prepare configuration for a Hass.io deCONZ bridge.

        This flow is triggered by the discovery component.
        """
        LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info.config))

        self.bridge_id = normalize_bridge_id(discovery_info.config[CONF_SERIAL])
        await self.async_set_unique_id(self.bridge_id)

        self._abort_if_unique_id_configured(
            updates={
                CONF_HOST: discovery_info.config[CONF_HOST],
                CONF_PORT: discovery_info.config[CONF_PORT],
                CONF_API_KEY: discovery_info.config[CONF_API_KEY],
            }
        )

        self.context["configuration_url"] = HASSIO_CONFIGURATION_URL

        self._hassio_discovery = discovery_info.config

        return await self.async_step_hassio_confirm()

    async def async_step_hassio_confirm(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Confirm a Hass.io discovery."""

        if user_input is not None:
            self.deconz_config = {
                CONF_HOST: self._hassio_discovery[CONF_HOST],
                CONF_PORT: self._hassio_discovery[CONF_PORT],
                CONF_API_KEY: self._hassio_discovery[CONF_API_KEY],
            }

            return await self._create_entry()

        return self.async_show_form(
            step_id="hassio_confirm",
            description_placeholders={"addon": self._hassio_discovery["addon"]},
        )


class DeconzOptionsFlowHandler(OptionsFlow):
    """Handle deCONZ options."""

    gateway: DeconzGateway

    def __init__(self, config_entry: ConfigEntry) -> None:
        """Initialize deCONZ options flow."""
        self.config_entry = config_entry
        self.options = dict(config_entry.options)

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Manage the deCONZ options."""
        self.gateway = get_gateway_from_config_entry(self.hass, self.config_entry)
        return await self.async_step_deconz_devices()

    async def async_step_deconz_devices(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Manage the deconz devices options."""
        if user_input is not None:
            self.options.update(user_input)
            return self.async_create_entry(title="", data=self.options)

        return self.async_show_form(
            step_id="deconz_devices",
            data_schema=vol.Schema(
                {
                    vol.Optional(
                        CONF_ALLOW_CLIP_SENSOR,
                        default=self.gateway.option_allow_clip_sensor,
                    ): bool,
                    vol.Optional(
                        CONF_ALLOW_DECONZ_GROUPS,
                        default=self.gateway.option_allow_deconz_groups,
                    ): bool,
                    vol.Optional(
                        CONF_ALLOW_NEW_DEVICES,
                        default=self.gateway.option_allow_new_devices,
                    ): bool,
                }
            ),
        )
