"""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


@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.
        """
        # 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_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,
            }
        )
