"""The nuki component."""

import asyncio
from http import HTTPStatus
import logging
from typing import Any

from aiohttp import web
from pynuki import NukiBridge, NukiLock, NukiOpener
from pynuki.bridge import InvalidCredentialsException
from requests.exceptions import RequestException

from homeassistant import exceptions
from homeassistant.components import webhook
from homeassistant.const import (
    CONF_HOST,
    CONF_PORT,
    CONF_TOKEN,
    EVENT_HOMEASSISTANT_STOP,
    Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.update_coordinator import UpdateFailed

from .const import CONF_ENCRYPT_TOKEN, DEFAULT_TIMEOUT, DOMAIN
from .coordinator import NukiConfigEntry, NukiCoordinator, NukiEntryData
from .helpers import NukiWebhookException, parse_id

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR]


def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOpener]]:
    return bridge.locks, bridge.openers


def _get_bridge_data(
    host: str, token: str, port: int, encrypted_token: bool
) -> tuple[NukiBridge, list[NukiLock], list[NukiOpener], dict[str, Any]]:
    """Get Nuki bridge and bridge data."""
    bridge = NukiBridge(host, token, port, encrypted_token, DEFAULT_TIMEOUT)
    locks, openers = _get_bridge_devices(bridge)
    return bridge, locks, openers, bridge.info()


async def _create_webhook(
    hass: HomeAssistant, entry: NukiConfigEntry, bridge: NukiBridge
) -> None:
    # Create HomeAssistant webhook
    async def handle_webhook(
        hass: HomeAssistant, webhook_id: str, request: web.Request
    ) -> web.Response:
        """Handle webhook callback."""
        try:
            data = await request.json()
        except ValueError:
            return web.Response(status=HTTPStatus.BAD_REQUEST)

        locks = entry.runtime_data.locks
        openers = entry.runtime_data.openers

        devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]]
        if len(devices) == 1:
            devices[0].update_from_callback(data)

        entry.runtime_data.coordinator.async_set_updated_data(None)

        return web.Response(status=HTTPStatus.OK)

    webhook.async_register(
        hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True
    )

    webhook_url = webhook.async_generate_path(entry.entry_id)

    try:
        hass_url = get_url(
            hass,
            allow_cloud=False,
            allow_external=False,
            allow_ip=True,
            require_ssl=False,
        )
    except NoURLAvailableError:
        webhook.async_unregister(hass, entry.entry_id)
        raise NukiWebhookException(
            f"Error registering URL for webhook {entry.entry_id}: "
            "HomeAssistant URL is not available"
        ) from None

    url = f"{hass_url}{webhook_url}"

    if hass_url.startswith("https"):
        ir.async_create_issue(
            hass,
            DOMAIN,
            "https_webhook",
            is_fixable=False,
            severity=ir.IssueSeverity.WARNING,
            translation_key="https_webhook",
            translation_placeholders={
                "base_url": hass_url,
                "network_link": "https://my.home-assistant.io/redirect/network/",
                "sample_ip": "192.168.1.10",
                "sample_url": "http://192.168.1.10:8123",
            },
        )
    else:
        ir.async_delete_issue(hass, DOMAIN, "https_webhook")

        try:
            async with asyncio.timeout(10):
                await hass.async_add_executor_job(
                    _register_webhook, bridge, entry.entry_id, url
                )
        except InvalidCredentialsException as err:
            webhook.async_unregister(hass, entry.entry_id)
            raise NukiWebhookException(
                f"Invalid credentials for Bridge: {err}"
            ) from err
        except RequestException as err:
            webhook.async_unregister(hass, entry.entry_id)
            raise NukiWebhookException(
                f"Error communicating with Bridge: {err}"
            ) from err


def _register_webhook(bridge: NukiBridge, entry_id: str, url: str) -> bool:
    # Register HA URL as webhook if not already
    callbacks = bridge.callback_list()
    for item in callbacks["callbacks"]:
        if entry_id in item["url"]:
            if item["url"] == url:
                return True
            bridge.callback_remove(item["id"])

    if bridge.callback_add(url)["success"]:
        return True

    return False


def _remove_webhook(bridge: NukiBridge, entry_id: str) -> None:
    # Remove webhook if set
    callbacks = bridge.callback_list()
    for item in callbacks["callbacks"]:
        if entry_id in item["url"]:
            bridge.callback_remove(item["id"])


async def async_setup_entry(hass: HomeAssistant, entry: NukiConfigEntry) -> bool:
    """Set up the Nuki entry."""

    # Migration of entry unique_id
    if isinstance(entry.unique_id, int):
        new_id = parse_id(entry.unique_id)
        params = {"unique_id": new_id}
        if entry.title == entry.unique_id:
            params["title"] = new_id
        hass.config_entries.async_update_entry(entry, **params)

    try:
        bridge, locks, openers, info = await hass.async_add_executor_job(
            _get_bridge_data,
            entry.data[CONF_HOST],
            entry.data[CONF_TOKEN],
            entry.data[CONF_PORT],
            entry.data.get(CONF_ENCRYPT_TOKEN, True),
        )
    except InvalidCredentialsException as err:
        raise exceptions.ConfigEntryAuthFailed from err
    except RequestException as err:
        raise exceptions.ConfigEntryNotReady from err

    # Device registration for the bridge
    bridge_id = parse_id(info["ids"]["hardwareId"])
    dev_reg = dr.async_get(hass)
    dev_reg.async_get_or_create(
        config_entry_id=entry.entry_id,
        identifiers={(DOMAIN, bridge_id)},
        manufacturer="Nuki Home Solutions GmbH",
        name=f"Nuki Bridge {bridge_id}",
        model="Hardware Bridge",
        sw_version=info["versions"]["firmwareVersion"],
        serial_number=parse_id(info["ids"]["hardwareId"]),
    )

    try:
        await _create_webhook(hass, entry, bridge)
    except NukiWebhookException as err:
        _LOGGER.warning("Error registering HomeAssistant webhook: %s", err)

    async def _stop_nuki(_: Event):
        """Stop and remove the Nuki webhook."""
        webhook.async_unregister(hass, entry.entry_id)
        try:
            async with asyncio.timeout(10):
                await hass.async_add_executor_job(
                    _remove_webhook, bridge, entry.entry_id
                )
        except InvalidCredentialsException as err:
            _LOGGER.error(
                "Error unregistering webhook, invalid credentials for bridge: %s", err
            )
        except RequestException as err:
            _LOGGER.error("Error communicating with bridge: %s", err)

    entry.async_on_unload(
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_nuki)
    )

    coordinator = NukiCoordinator(hass, entry, bridge, locks, openers)
    entry.runtime_data = NukiEntryData(
        coordinator=coordinator,
        bridge=bridge,
        locks=locks,
        openers=openers,
    )

    # Fetch initial data so we have data when entities subscribe
    await coordinator.async_refresh()

    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    return True


async def async_unload_entry(hass: HomeAssistant, entry: NukiConfigEntry) -> bool:
    """Unload the Nuki entry."""
    webhook.async_unregister(hass, entry.entry_id)

    try:
        async with asyncio.timeout(10):
            await hass.async_add_executor_job(
                _remove_webhook,
                entry.runtime_data.bridge,
                entry.entry_id,
            )
    except InvalidCredentialsException as err:
        raise UpdateFailed(
            f"Unable to remove callback. Invalid credentials for Bridge: {err}"
        ) from err
    except RequestException as err:
        raise UpdateFailed(
            f"Unable to remove callback. Error communicating with Bridge: {err}"
        ) from err

    return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
