"""Config flow for AlarmDecoder."""
import logging

from adext import AdExt
from alarmdecoder.devices import SerialDevice, SocketDevice
from alarmdecoder.util import NoDeviceError
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components.binary_sensor import DEVICE_CLASSES
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
from homeassistant.core import callback

from .const import (
    CONF_ALT_NIGHT_MODE,
    CONF_AUTO_BYPASS,
    CONF_CODE_ARM_REQUIRED,
    CONF_DEVICE_BAUD,
    CONF_DEVICE_PATH,
    CONF_RELAY_ADDR,
    CONF_RELAY_CHAN,
    CONF_ZONE_LOOP,
    CONF_ZONE_NAME,
    CONF_ZONE_NUMBER,
    CONF_ZONE_RFID,
    CONF_ZONE_TYPE,
    DEFAULT_ARM_OPTIONS,
    DEFAULT_DEVICE_BAUD,
    DEFAULT_DEVICE_HOST,
    DEFAULT_DEVICE_PATH,
    DEFAULT_DEVICE_PORT,
    DEFAULT_ZONE_OPTIONS,
    DEFAULT_ZONE_TYPE,
    DOMAIN,
    OPTIONS_ARM,
    OPTIONS_ZONES,
    PROTOCOL_SERIAL,
    PROTOCOL_SOCKET,
)

EDIT_KEY = "edit_selection"
EDIT_ZONES = "Zones"
EDIT_SETTINGS = "Arming Settings"

_LOGGER = logging.getLogger(__name__)


class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
    """Handle a AlarmDecoder config flow."""

    VERSION = 1

    def __init__(self):
        """Initialize AlarmDecoder ConfigFlow."""
        self.protocol = None

    @staticmethod
    @callback
    def async_get_options_flow(config_entry):
        """Get the options flow for AlarmDecoder."""
        return AlarmDecoderOptionsFlowHandler(config_entry)

    async def async_step_user(self, user_input=None):
        """Handle a flow initialized by the user."""
        if user_input is not None:
            self.protocol = user_input[CONF_PROTOCOL]
            return await self.async_step_protocol()

        return self.async_show_form(
            step_id="user",
            data_schema=vol.Schema(
                {
                    vol.Required(CONF_PROTOCOL): vol.In(
                        [PROTOCOL_SOCKET, PROTOCOL_SERIAL]
                    ),
                }
            ),
        )

    async def async_step_protocol(self, user_input=None):
        """Handle AlarmDecoder protocol setup."""
        errors = {}
        if user_input is not None:
            if _device_already_added(
                self._async_current_entries(), user_input, self.protocol
            ):
                return self.async_abort(reason="already_configured")
            connection = {}
            baud = None
            if self.protocol == PROTOCOL_SOCKET:
                host = connection[CONF_HOST] = user_input[CONF_HOST]
                port = connection[CONF_PORT] = user_input[CONF_PORT]
                title = f"{host}:{port}"
                device = SocketDevice(interface=(host, port))
            if self.protocol == PROTOCOL_SERIAL:
                path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
                baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
                title = path
                device = SerialDevice(interface=path)

            controller = AdExt(device)

            def test_connection():
                controller.open(baud)
                controller.close()

            try:
                await self.hass.async_add_executor_job(test_connection)
                return self.async_create_entry(
                    title=title, data={CONF_PROTOCOL: self.protocol, **connection}
                )
            except NoDeviceError:
                errors["base"] = "cannot_connect"
            except Exception:  # pylint: disable=broad-except
                _LOGGER.exception("Unexpected exception during AlarmDecoder setup")
                errors["base"] = "unknown"

        if self.protocol == PROTOCOL_SOCKET:
            schema = vol.Schema(
                {
                    vol.Required(CONF_HOST, default=DEFAULT_DEVICE_HOST): str,
                    vol.Required(CONF_PORT, default=DEFAULT_DEVICE_PORT): int,
                }
            )
        if self.protocol == PROTOCOL_SERIAL:
            schema = vol.Schema(
                {
                    vol.Required(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): str,
                    vol.Required(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): int,
                }
            )

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


class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
    """Handle AlarmDecoder options."""

    def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
        """Initialize AlarmDecoder options flow."""
        self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
        self.zone_options = config_entry.options.get(
            OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
        )
        self.selected_zone = None

    async def async_step_init(self, user_input=None):
        """Manage the options."""
        if user_input is not None:
            if user_input[EDIT_KEY] == EDIT_SETTINGS:
                return await self.async_step_arm_settings()
            if user_input[EDIT_KEY] == EDIT_ZONES:
                return await self.async_step_zone_select()

        return self.async_show_form(
            step_id="init",
            data_schema=vol.Schema(
                {
                    vol.Required(EDIT_KEY, default=EDIT_SETTINGS): vol.In(
                        [EDIT_SETTINGS, EDIT_ZONES]
                    )
                },
            ),
        )

    async def async_step_arm_settings(self, user_input=None):
        """Arming options form."""
        if user_input is not None:
            return self.async_create_entry(
                title="",
                data={OPTIONS_ARM: user_input, OPTIONS_ZONES: self.zone_options},
            )

        return self.async_show_form(
            step_id="arm_settings",
            data_schema=vol.Schema(
                {
                    vol.Optional(
                        CONF_ALT_NIGHT_MODE,
                        default=self.arm_options[CONF_ALT_NIGHT_MODE],
                    ): bool,
                    vol.Optional(
                        CONF_AUTO_BYPASS, default=self.arm_options[CONF_AUTO_BYPASS]
                    ): bool,
                    vol.Optional(
                        CONF_CODE_ARM_REQUIRED,
                        default=self.arm_options[CONF_CODE_ARM_REQUIRED],
                    ): bool,
                },
            ),
        )

    async def async_step_zone_select(self, user_input=None):
        """Zone selection form."""
        errors = _validate_zone_input(user_input)

        if user_input is not None and not errors:
            self.selected_zone = str(
                int(user_input[CONF_ZONE_NUMBER])
            )  # remove leading zeros
            return await self.async_step_zone_details()

        return self.async_show_form(
            step_id="zone_select",
            data_schema=vol.Schema({vol.Required(CONF_ZONE_NUMBER): str}),
            errors=errors,
        )

    async def async_step_zone_details(self, user_input=None):
        """Zone details form."""
        errors = _validate_zone_input(user_input)

        if user_input is not None and not errors:
            zone_options = self.zone_options.copy()
            zone_id = self.selected_zone
            zone_options[zone_id] = _fix_input_types(user_input)

            # Delete zone entry if zone_name is omitted
            if CONF_ZONE_NAME not in zone_options[zone_id]:
                zone_options.pop(zone_id)

            return self.async_create_entry(
                title="",
                data={OPTIONS_ARM: self.arm_options, OPTIONS_ZONES: zone_options},
            )

        existing_zone_settings = self.zone_options.get(self.selected_zone, {})

        return self.async_show_form(
            step_id="zone_details",
            description_placeholders={CONF_ZONE_NUMBER: self.selected_zone},
            data_schema=vol.Schema(
                {
                    vol.Optional(
                        CONF_ZONE_NAME,
                        description={
                            "suggested_value": existing_zone_settings.get(
                                CONF_ZONE_NAME
                            )
                        },
                    ): str,
                    vol.Optional(
                        CONF_ZONE_TYPE,
                        default=existing_zone_settings.get(
                            CONF_ZONE_TYPE, DEFAULT_ZONE_TYPE
                        ),
                    ): vol.In(DEVICE_CLASSES),
                    vol.Optional(
                        CONF_ZONE_RFID,
                        description={
                            "suggested_value": existing_zone_settings.get(
                                CONF_ZONE_RFID
                            )
                        },
                    ): str,
                    vol.Optional(
                        CONF_ZONE_LOOP,
                        description={
                            "suggested_value": existing_zone_settings.get(
                                CONF_ZONE_LOOP
                            )
                        },
                    ): str,
                    vol.Optional(
                        CONF_RELAY_ADDR,
                        description={
                            "suggested_value": existing_zone_settings.get(
                                CONF_RELAY_ADDR
                            )
                        },
                    ): str,
                    vol.Optional(
                        CONF_RELAY_CHAN,
                        description={
                            "suggested_value": existing_zone_settings.get(
                                CONF_RELAY_CHAN
                            )
                        },
                    ): str,
                }
            ),
            errors=errors,
        )


def _validate_zone_input(zone_input):
    if not zone_input:
        return {}
    errors = {}

    # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive
    if (CONF_RELAY_ADDR in zone_input and CONF_RELAY_CHAN not in zone_input) or (
        CONF_RELAY_ADDR not in zone_input and CONF_RELAY_CHAN in zone_input
    ):
        errors["base"] = "relay_inclusive"

    # The following keys must be int
    for key in (CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN):
        if key in zone_input:
            try:
                int(zone_input[key])
            except ValueError:
                errors[key] = "int"

    # CONF_ZONE_LOOP depends on CONF_ZONE_RFID
    if CONF_ZONE_LOOP in zone_input and CONF_ZONE_RFID not in zone_input:
        errors[CONF_ZONE_LOOP] = "loop_rfid"

    # CONF_ZONE_LOOP must be 1-4
    if (
        CONF_ZONE_LOOP in zone_input
        and zone_input[CONF_ZONE_LOOP].isdigit()
        and int(zone_input[CONF_ZONE_LOOP]) not in list(range(1, 5))
    ):
        errors[CONF_ZONE_LOOP] = "loop_range"

    return errors


def _fix_input_types(zone_input):
    """Convert necessary keys to int.

    Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
    strings and then convert them to ints.
    """

    for key in (CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN):
        if key in zone_input:
            zone_input[key] = int(zone_input[key])

    return zone_input


def _device_already_added(current_entries, user_input, protocol):
    """Determine if entry has already been added to HA."""
    user_host = user_input.get(CONF_HOST)
    user_port = user_input.get(CONF_PORT)
    user_path = user_input.get(CONF_DEVICE_PATH)
    user_baud = user_input.get(CONF_DEVICE_BAUD)

    for entry in current_entries:
        entry_host = entry.data.get(CONF_HOST)
        entry_port = entry.data.get(CONF_PORT)
        entry_path = entry.data.get(CONF_DEVICE_PATH)
        entry_baud = entry.data.get(CONF_DEVICE_BAUD)

        if (
            protocol == PROTOCOL_SOCKET
            and user_host == entry_host
            and user_port == entry_port
        ):
            return True

        if (
            protocol == PROTOCOL_SERIAL
            and user_baud == entry_baud
            and user_path == entry_path
        ):
            return True

    return False