"""Config flow to configure Philips Hue."""
import asyncio
import json
import os

from aiohue.discovery import discover_nupnp
import async_timeout
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client

from .bridge import get_bridge
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect

HUE_MANUFACTURERURL = "http://www.philips.com"


@callback
def configured_hosts(hass):
    """Return a set of the configured hosts."""
    return set(
        entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN)
    )


def _find_username_from_config(hass, filename):
    """Load username from config.

    This was a legacy way of configuring Hue until Home Assistant 0.67.
    """
    path = hass.config.path(filename)

    if not os.path.isfile(path):
        return None

    with open(path) as inp:
        try:
            return list(json.load(inp).values())[0]["username"]
        except ValueError:
            # If we get invalid JSON
            return None


@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlow):
    """Handle a Hue config flow."""

    VERSION = 1
    CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

    def __init__(self):
        """Initialize the Hue flow."""
        self.host = None

    async def async_step_user(self, user_input=None):
        """Handle a flow initialized by the user."""
        return await self.async_step_init(user_input)

    async def async_step_init(self, user_input=None):
        """Handle a flow start."""
        if user_input is not None:
            self.host = user_input["host"]
            return await self.async_step_link()

        websession = aiohttp_client.async_get_clientsession(self.hass)

        try:
            with async_timeout.timeout(5):
                bridges = await discover_nupnp(websession=websession)
        except asyncio.TimeoutError:
            return self.async_abort(reason="discover_timeout")

        if not bridges:
            return self.async_abort(reason="no_bridges")

        # Find already configured hosts
        configured = configured_hosts(self.hass)

        hosts = [bridge.host for bridge in bridges if bridge.host not in configured]

        if not hosts:
            return self.async_abort(reason="all_configured")

        if len(hosts) == 1:
            self.host = hosts[0]
            return await self.async_step_link()

        return self.async_show_form(
            step_id="init",
            data_schema=vol.Schema({vol.Required("host"): vol.In(hosts)}),
        )

    async def async_step_link(self, user_input=None):
        """Attempt to link with the Hue bridge.

        Given a configured host, will ask the user to press the link button
        to connect to the bridge.
        """
        errors = {}

        # We will always try linking in case the user has already pressed
        # the link button.
        try:
            bridge = await get_bridge(self.hass, self.host, username=None)

            return await self._entry_from_bridge(bridge)
        except AuthenticationRequired:
            errors["base"] = "register_failed"

        except CannotConnect:
            LOGGER.error("Error connecting to the Hue bridge at %s", self.host)
            errors["base"] = "linking"

        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Unknown error connecting with Hue bridge at %s", self.host
            )
            errors["base"] = "linking"

        # If there was no user input, do not show the errors.
        if user_input is None:
            errors = {}

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

    async def async_step_ssdp(self, discovery_info):
        """Handle a discovered Hue bridge.

        This flow is triggered by the SSDP component. It will check if the
        host is already configured and delegate to the import step if not.
        """
        from homeassistant.components.ssdp import ATTR_MANUFACTURERURL

        if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
            return self.async_abort(reason="not_hue_bridge")

        # Filter out emulated Hue
        if "HASS Bridge" in discovery_info.get("name", ""):
            return self.async_abort(reason="already_configured")

        # pylint: disable=unsupported-assignment-operation
        host = self.context["host"] = discovery_info.get("host")

        if any(host == flow["context"]["host"] for flow in self._async_in_progress()):
            return self.async_abort(reason="already_in_progress")

        if host in configured_hosts(self.hass):
            return self.async_abort(reason="already_configured")

        # This value is based off host/description.xml and is, weirdly, missing
        # 4 characters in the middle of the serial compared to results returned
        # from the NUPNP API or when querying the bridge API for bridgeid.
        # (on first gen Hue hub)
        serial = discovery_info.get("serial")

        return await self.async_step_import(
            {
                "host": host,
                # This format is the legacy format that Hue used for discovery
                "path": "phue-{}.conf".format(serial),
            }
        )

    async def async_step_homekit(self, homekit_info):
        """Handle HomeKit discovery."""
        # pylint: disable=unsupported-assignment-operation
        host = self.context["host"] = homekit_info.get("host")

        if any(host == flow["context"]["host"] for flow in self._async_in_progress()):
            return self.async_abort(reason="already_in_progress")

        if host in configured_hosts(self.hass):
            return self.async_abort(reason="already_configured")

        return await self.async_step_import({"host": host})

    async def async_step_import(self, import_info):
        """Import a new bridge as a config entry.

        Will read authentication from Phue config file if available.

        This flow is triggered by `async_setup` for both configured and
        discovered bridges. Triggered for any bridge that does not have a
        config entry yet (based on host).

        This flow is also triggered by `async_step_discovery`.

        If an existing config file is found, we will validate the credentials
        and create an entry. Otherwise we will delegate to `link` step which
        will ask user to link the bridge.
        """
        host = import_info["host"]
        path = import_info.get("path")

        if path is not None:
            username = await self.hass.async_add_job(
                _find_username_from_config, self.hass, self.hass.config.path(path)
            )
        else:
            username = None

        try:
            bridge = await get_bridge(self.hass, host, username)

            LOGGER.info("Imported authentication for %s from %s", host, path)

            return await self._entry_from_bridge(bridge)
        except AuthenticationRequired:
            self.host = host

            LOGGER.info("Invalid authentication for %s, requesting link.", host)

            return await self.async_step_link()

        except CannotConnect:
            LOGGER.error("Error connecting to the Hue bridge at %s", host)
            return self.async_abort(reason="cannot_connect")

        except Exception:  # pylint: disable=broad-except
            LOGGER.exception("Unknown error connecting with Hue bridge at %s", host)
            return self.async_abort(reason="unknown")

    async def _entry_from_bridge(self, bridge):
        """Return a config entry from an initialized bridge."""
        # Remove all other entries of hubs with same ID or host
        host = bridge.host
        bridge_id = bridge.config.bridgeid

        same_hub_entries = [
            entry.entry_id
            for entry in self.hass.config_entries.async_entries(DOMAIN)
            if entry.data["bridge_id"] == bridge_id or entry.data["host"] == host
        ]

        if same_hub_entries:
            await asyncio.wait(
                [
                    self.hass.config_entries.async_remove(entry_id)
                    for entry_id in same_hub_entries
                ]
            )

        return self.async_create_entry(
            title=bridge.config.name,
            data={"host": host, "bridge_id": bridge_id, "username": bridge.username},
        )
