"""Manager to set up IO with Crownstone devices for a config entry."""

import logging
from typing import Any

from crownstone_cloud import CrownstoneCloud
from crownstone_cloud.exceptions import (
    CrownstoneAuthenticationError,
    CrownstoneUnknownError,
)
from crownstone_sse import CrownstoneSSEAsync
from crownstone_uart import CrownstoneUart, UartEventBus
from crownstone_uart.Exceptions import UartException

from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import async_dispatcher_send

from .const import (
    CONF_USB_PATH,
    CONF_USB_SPHERE,
    DOMAIN,
    PROJECT_NAME,
    SSE_LISTENERS,
    UART_LISTENERS,
)
from .helpers import get_port
from .listeners import setup_sse_listeners, setup_uart_listeners

_LOGGER = logging.getLogger(__name__)

type CrownstoneConfigEntry = ConfigEntry[CrownstoneEntryManager]


class CrownstoneEntryManager:
    """Manage a Crownstone config entry."""

    uart: CrownstoneUart | None = None
    cloud: CrownstoneCloud
    sse: CrownstoneSSEAsync

    def __init__(
        self, hass: HomeAssistant, config_entry: CrownstoneConfigEntry
    ) -> None:
        """Initialize the hub."""
        self.hass = hass
        self.config_entry = config_entry
        self.listeners: dict[str, Any] = {}
        self.usb_sphere_id: str | None = None

    async def async_setup(self) -> bool:
        """Set up a Crownstone config entry.

        Returns True if the setup was successful.
        """
        email = self.config_entry.data[CONF_EMAIL]
        password = self.config_entry.data[CONF_PASSWORD]

        self.cloud = CrownstoneCloud(
            email=email,
            password=password,
            clientsession=aiohttp_client.async_get_clientsession(self.hass),
        )
        # Login & sync all user data
        try:
            await self.cloud.async_initialize()
        except CrownstoneAuthenticationError as auth_err:
            _LOGGER.error(
                "Auth error during login with type: %s and message: %s",
                auth_err.type,
                auth_err.message,
            )
            return False
        except CrownstoneUnknownError as unknown_err:
            _LOGGER.error("Unknown error during login")
            raise ConfigEntryNotReady from unknown_err

        # A new clientsession is created because the default
        # one does not cleanup on unload
        self.sse = CrownstoneSSEAsync(
            email=email,
            password=password,
            access_token=self.cloud.access_token,
            websession=aiohttp_client.async_create_clientsession(self.hass),
            project_name=PROJECT_NAME,
        )
        # Listen for events in the background, without task tracking
        self.config_entry.async_create_background_task(
            self.hass, self.async_process_events(self.sse), "crownstone-sse"
        )
        setup_sse_listeners(self)

        # Set up a Crownstone USB only if path exists
        if self.config_entry.options[CONF_USB_PATH] is not None:
            await self.async_setup_usb()

        # Save the sphere where the USB is located
        # Makes HA aware of the Crownstone environment HA is
        # placed in, a user can have multiple
        self.usb_sphere_id = self.config_entry.options[CONF_USB_SPHERE]

        return True

    async def async_process_events(self, sse_client: CrownstoneSSEAsync) -> None:
        """Asynchronous iteration of Crownstone SSE events."""
        async with sse_client as client:
            async for event in client:
                if event is not None:
                    async_dispatcher_send(self.hass, f"{DOMAIN}_{event.type}", event)

    async def async_setup_usb(self) -> None:
        """Attempt setup of a Crownstone usb dongle."""
        # Trace by-id symlink back to the serial port
        serial_port = await self.hass.async_add_executor_job(
            get_port, self.config_entry.options[CONF_USB_PATH]
        )
        if serial_port is None:
            return

        self.uart = CrownstoneUart()
        # UartException is raised when serial controller fails to open
        try:
            await self.uart.initialize_usb(serial_port)
        except UartException:
            self.uart = None
            # Set entry options for usb to null
            updated_options = self.config_entry.options.copy()
            updated_options[CONF_USB_PATH] = None
            updated_options[CONF_USB_SPHERE] = None
            # Ensure that the user can configure an USB again from options
            self.hass.config_entries.async_update_entry(
                self.config_entry, options=updated_options
            )
            # Show notification to ensure the user knows the cloud is now used
            persistent_notification.async_create(
                self.hass,
                (
                    "Setup of Crownstone USB dongle was unsuccessful on port"
                    f" {serial_port}.\n Crownstone Cloud will be used"
                    " to switch Crownstones.\n Please check if your"
                    " port is correct and set up the USB again from integration"
                    " options."
                ),
                "Crownstone",
                "crownstone_usb_dongle_setup",
            )
            return

        setup_uart_listeners(self)

    @callback
    def async_unload(self) -> None:
        """Unload the current config entry."""
        # Authentication failed
        if self.cloud.cloud_data is None:
            return

        self.sse.close_client()
        for sse_unsub in self.listeners[SSE_LISTENERS]:
            sse_unsub()

        if self.uart:
            self.uart.stop()
            for subscription_id in self.listeners[UART_LISTENERS]:
                UartEventBus.unsubscribe(subscription_id)

    @callback
    def on_shutdown(self, _: Event) -> None:
        """Close all IO connections."""
        self.sse.close_client()
        if self.uart:
            self.uart.stop()
