"""Config flow for Nobø Ecohub integration."""
from __future__ import annotations

import socket
from typing import Any

from pynobo import nobo
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError

from .const import (
    CONF_AUTO_DISCOVERED,
    CONF_OVERRIDE_TYPE,
    CONF_SERIAL,
    DOMAIN,
    OVERRIDE_TYPE_CONSTANT,
    OVERRIDE_TYPE_NOW,
)

DATA_NOBO_HUB_IMPL = "nobo_hub_flow_implementation"
DEVICE_INPUT = "device_input"


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    """Handle a config flow for Nobø Ecohub."""

    VERSION = 1

    def __init__(self):
        """Initialize the config flow."""
        self._discovered_hubs = None
        self._hub = None

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle the initial step."""
        if self._discovered_hubs is None:
            self._discovered_hubs = dict(await nobo.async_discover_hubs())

        if not self._discovered_hubs:
            # No hubs auto discovered
            return await self.async_step_manual()

        if user_input is not None:
            if user_input["device"] == "manual":
                return await self.async_step_manual()
            self._hub = user_input["device"]
            return await self.async_step_selected()

        hubs = self._hubs()
        hubs["manual"] = "Manual"
        data_schema = vol.Schema(
            {
                vol.Required("device"): vol.In(hubs),
            }
        )
        return self.async_show_form(
            step_id="user",
            data_schema=data_schema,
        )

    async def async_step_selected(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle configuration of a selected discovered device."""
        errors = {}
        if user_input is not None:
            serial_prefix = self._discovered_hubs[self._hub]
            serial_suffix = user_input["serial_suffix"]
            serial = f"{serial_prefix}{serial_suffix}"
            try:
                return await self._create_configuration(serial, self._hub, True)
            except NoboHubConnectError as error:
                errors["base"] = error.msg

        user_input = user_input or {}
        return self.async_show_form(
            step_id="selected",
            data_schema=vol.Schema(
                {
                    vol.Required(
                        "serial_suffix", default=user_input.get("serial_suffix")
                    ): str,
                }
            ),
            errors=errors,
            description_placeholders={
                "hub": self._format_hub(self._hub, self._discovered_hubs[self._hub])
            },
        )

    async def async_step_manual(
        self, user_input: dict[str, Any] | None = None
    ) -> FlowResult:
        """Handle configuration of an undiscovered device."""
        errors = {}
        if user_input is not None:
            serial = user_input[CONF_SERIAL]
            ip_address = user_input[CONF_IP_ADDRESS]
            try:
                return await self._create_configuration(serial, ip_address, False)
            except NoboHubConnectError as error:
                errors["base"] = error.msg

        user_input = user_input or {}
        return self.async_show_form(
            step_id="manual",
            data_schema=vol.Schema(
                {
                    vol.Required(CONF_SERIAL, default=user_input.get(CONF_SERIAL)): str,
                    vol.Required(
                        CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS)
                    ): str,
                }
            ),
            errors=errors,
        )

    async def _create_configuration(
        self, serial: str, ip_address: str, auto_discovered: bool
    ) -> FlowResult:
        await self.async_set_unique_id(serial)
        self._abort_if_unique_id_configured()
        name = await self._test_connection(serial, ip_address)
        return self.async_create_entry(
            title=name,
            data={
                CONF_SERIAL: serial,
                CONF_IP_ADDRESS: ip_address,
                CONF_AUTO_DISCOVERED: auto_discovered,
            },
        )

    async def _test_connection(self, serial: str, ip_address: str) -> str:
        if not len(serial) == 12 or not serial.isdigit():
            raise NoboHubConnectError("invalid_serial")
        try:
            socket.inet_aton(ip_address)
        except OSError as err:
            raise NoboHubConnectError("invalid_ip") from err
        hub = nobo(serial=serial, ip=ip_address, discover=False, synchronous=False)
        if not await hub.async_connect_hub(ip_address, serial):
            raise NoboHubConnectError("cannot_connect")
        name = hub.hub_info["name"]
        await hub.close()
        return name

    @staticmethod
    def _format_hub(ip, serial_prefix):
        return f"{serial_prefix}XXX ({ip})"

    def _hubs(self):
        return {
            ip: self._format_hub(ip, serial_prefix)
            for ip, serial_prefix in self._discovered_hubs.items()
        }

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


class NoboHubConnectError(HomeAssistantError):
    """Error with connecting to Nobø Ecohub."""

    def __init__(self, msg) -> None:
        """Instantiate error."""
        super().__init__()
        self.msg = msg


class OptionsFlowHandler(config_entries.OptionsFlow):
    """Handles options flow for the component."""

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

    async def async_step_init(self, user_input=None) -> FlowResult:
        """Manage the options."""

        if user_input is not None:
            data = {
                CONF_OVERRIDE_TYPE: user_input.get(CONF_OVERRIDE_TYPE),
            }
            return self.async_create_entry(title="", data=data)

        override_type = self.config_entry.options.get(
            CONF_OVERRIDE_TYPE, OVERRIDE_TYPE_CONSTANT
        )

        schema = vol.Schema(
            {
                vol.Required(CONF_OVERRIDE_TYPE, default=override_type): vol.In(
                    [OVERRIDE_TYPE_CONSTANT, OVERRIDE_TYPE_NOW]
                ),
            }
        )

        return self.async_show_form(step_id="init", data_schema=schema)