"""Support the Universal Devices ISY/IoX controllers."""

import asyncio
from urllib.parse import urlparse

from aiohttp import CookieJar
from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
from pyisy.constants import CONFIG_NETWORKING, CONFIG_PORTAL
import voluptuous as vol

from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
    CONF_USERNAME,
    CONF_VARIABLES,
    CONF_VERIFY_SSL,
    EVENT_HOMEASSISTANT_STOP,
    Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
    aiohttp_client,
    config_validation as cv,
    device_registry as dr,
)
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.typing import ConfigType

from .const import (
    CONF_IGNORE_STRING,
    CONF_NETWORK,
    CONF_SENSOR_STRING,
    DEFAULT_IGNORE_STRING,
    DEFAULT_SENSOR_STRING,
    DEFAULT_VERIFY_SSL,
    DOMAIN,
    ISY_CONF_FIRMWARE,
    ISY_CONF_MODEL,
    ISY_CONF_NAME,
    LOGGER,
    MANUFACTURER,
    PLATFORMS,
    SCHEME_HTTP,
    SCHEME_HTTPS,
)
from .helpers import _categorize_nodes, _categorize_programs
from .models import IsyConfigEntry, IsyData
from .services import async_setup_services
from .util import _async_cleanup_registry_entries

CONFIG_SCHEMA = vol.Schema(
    cv.deprecated(DOMAIN),
    extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the ISY 994 integration."""

    async_setup_services(hass)

    return True


async def async_migrate_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
    """Migrate old config entries."""
    if entry.version == 1 and entry.minor_version == 1:
        new_data = {key: value for key, value in entry.data.items() if key != "tls"}
        new_data.setdefault(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
        hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)

    return True


async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
    """Set up the ISY 994 integration."""
    isy_config = entry.data
    isy_options = entry.options

    # Required
    user = isy_config[CONF_USERNAME]
    password = isy_config[CONF_PASSWORD]
    host = urlparse(isy_config[CONF_HOST])
    verify_ssl = isy_config[CONF_VERIFY_SSL]
    ignore_identifier = isy_options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
    sensor_identifier = isy_options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)

    if host.scheme == SCHEME_HTTP:
        https = False
        port = host.port or 80
        session = aiohttp_client.async_create_clientsession(
            hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
        )
    elif host.scheme == SCHEME_HTTPS:
        https = True
        port = host.port or 443
        session = aiohttp_client.async_get_clientsession(hass, verify_ssl=verify_ssl)
    else:
        LOGGER.error("The ISY/IoX host value in configuration is invalid")
        return False

    # Connect to ISY controller.
    isy = ISY(
        host.hostname,
        port,
        username=user,
        password=password,
        use_https=https,
        verify_ssl=verify_ssl,
        webroot=host.path,
        websession=session,
        use_websocket=True,
    )

    try:
        async with asyncio.timeout(60):
            await isy.initialize()
    except TimeoutError as err:
        raise ConfigEntryNotReady(
            "Timed out initializing the ISY; device may be busy, trying again later:"
            f" {err}"
        ) from err
    except ISYInvalidAuthError as err:
        raise ConfigEntryAuthFailed(f"Invalid credentials for the ISY: {err}") from err
    except ISYConnectionError as err:
        raise ConfigEntryNotReady(
            f"Failed to connect to the ISY, please adjust settings and try again: {err}"
        ) from err
    except ISYResponseParseError as err:
        raise ConfigEntryNotReady(
            "Invalid XML response from ISY; Ensure the ISY is running the latest"
            f" firmware: {err}"
        ) from err
    except TypeError as err:
        raise ConfigEntryNotReady(
            f"Invalid response ISY, device is likely still starting: {err}"
        ) from err

    isy_data = entry.runtime_data = IsyData()
    isy_data.root = isy
    _async_get_or_create_isy_device_in_registry(hass, entry, isy)
    via_device_id = dr.async_get_device_id_by_identifier(
        hass, (DOMAIN, isy.uuid), config_entry_id=entry.entry_id
    )
    _categorize_nodes(
        isy_data, isy.nodes, ignore_identifier, sensor_identifier, via_device_id
    )
    _categorize_programs(isy_data, isy.programs)
    # Gather ISY Variables to be added.
    if isy.variables.children:
        isy_data.devices[CONF_VARIABLES] = _create_service_device_info(
            isy,
            name=CONF_VARIABLES.title(),
            unique_id=CONF_VARIABLES,
            via_device_id=via_device_id,
        )
        numbers = isy_data.variables[Platform.NUMBER]
        for vtype, _, vid in isy.variables.children:
            numbers.append(isy.variables[vtype][vid])
    if (
        isy.conf[CONFIG_NETWORKING] or isy.conf.get(CONFIG_PORTAL)
    ) and isy.networking.nobjs:
        isy_data.devices[CONF_NETWORK] = _create_service_device_info(
            isy,
            name=CONFIG_NETWORKING,
            unique_id=CONF_NETWORK,
            via_device_id=via_device_id,
        )
        for resource in isy.networking.nobjs:
            isy_data.net_resources.append(resource)

    # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
    LOGGER.debug(repr(isy.clock))

    # Load platforms for the devices in the ISY controller that we support.
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    # Clean-up any old entities that we no longer provide.
    _async_cleanup_registry_entries(hass, entry)

    @callback
    def _async_stop_auto_update(event: Event) -> None:
        """Stop the isy auto update on Home Assistant Shutdown."""
        LOGGER.debug("ISY Stopping Event Stream and automatic updates")
        isy.websocket.stop()

    LOGGER.debug("ISY Starting Event Stream and automatic updates")
    isy.websocket.start()

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

    return True


@callback
def _async_get_or_create_isy_device_in_registry(
    hass: HomeAssistant, entry: IsyConfigEntry, isy: ISY
) -> None:
    device_registry = dr.async_get(hass)
    device_registry.async_get_or_create(
        config_entry_id=entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, isy.uuid)},
        identifiers={(DOMAIN, isy.uuid)},
        manufacturer=MANUFACTURER,
        name=isy.conf[ISY_CONF_NAME],
        model=isy.conf[ISY_CONF_MODEL],
        sw_version=isy.conf[ISY_CONF_FIRMWARE],
        configuration_url=isy.conn.url,
    )


def _create_service_device_info(
    isy: ISY, name: str, unique_id: str, via_device_id: str | None
) -> DeviceInfo:
    """Create device info for ISY service devices."""
    device_info = DeviceInfo(
        identifiers={
            (
                DOMAIN,
                f"{isy.uuid}_{unique_id}",
            )
        },
        manufacturer=MANUFACTURER,
        name=f"{isy.conf[ISY_CONF_NAME]} {name}",
        model=isy.conf[ISY_CONF_MODEL],
        sw_version=isy.conf[ISY_CONF_FIRMWARE],
        configuration_url=isy.conn.url,
        entry_type=DeviceEntryType.SERVICE,
    )
    if via_device_id is not None:
        device_info["via_device_id"] = via_device_id
    return device_info


async def async_unload_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
    """Unload a config entry."""
    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

    LOGGER.debug("ISY Stopping Event Stream and automatic updates")
    entry.runtime_data.root.websocket.stop()

    return unload_ok


async def async_remove_config_entry_device(
    hass: HomeAssistant,
    config_entry: IsyConfigEntry,
    device_entry: dr.DeviceEntry,
) -> bool:
    """Remove ISY config entry from a device."""
    return not device_entry.identifiers.intersection(
        (DOMAIN, unique_id) for unique_id in config_entry.runtime_data.devices
    )
