"""Support for Tasmota device discovery."""
from __future__ import annotations

from collections.abc import Awaitable, Callable
import logging
from typing import TypedDict, cast

from hatasmota import const as tasmota_const
from hatasmota.discovery import (
    TasmotaDiscovery,
    get_device_config as tasmota_get_device_config,
    get_entities_for_platform as tasmota_get_entities_for_platform,
    get_entity as tasmota_get_entity,
    get_trigger as tasmota_get_trigger,
    get_triggers as tasmota_get_triggers,
    unique_id_from_hash,
)
from hatasmota.entity import TasmotaEntityConfig
from hatasmota.models import DiscoveryHashType, TasmotaDeviceConfig
from hatasmota.mqtt import TasmotaMQTTClient
from hatasmota.sensor import TasmotaBaseSensorConfig
from hatasmota.utils import get_topic_command, get_topic_stat

from homeassistant.components import sensor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
    device_registry as dr,
    entity_registry as er,
    issue_registry as ir,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_registry import async_entries_for_device

from .const import DOMAIN, PLATFORMS

_LOGGER = logging.getLogger(__name__)

ALREADY_DISCOVERED = "tasmota_discovered_components"
DISCOVERY_DATA = "tasmota_discovery_data"
TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}"
TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}"
TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance"

MQTT_TOPIC_URL = "https://tasmota.github.io/docs/Home-Assistant/#tasmota-integration"

SetupDeviceCallback = Callable[[TasmotaDeviceConfig, str], Awaitable[None]]


def clear_discovery_hash(
    hass: HomeAssistant, discovery_hash: DiscoveryHashType
) -> None:
    """Clear entry in ALREADY_DISCOVERED list."""
    if ALREADY_DISCOVERED not in hass.data:
        # Discovery is shutting down
        return
    del hass.data[ALREADY_DISCOVERED][discovery_hash]


def set_discovery_hash(hass: HomeAssistant, discovery_hash: DiscoveryHashType) -> None:
    """Set entry in ALREADY_DISCOVERED list."""
    hass.data[ALREADY_DISCOVERED][discovery_hash] = {}


def warn_if_topic_duplicated(
    hass: HomeAssistant,
    command_topic: str,
    own_mac: str | None,
    own_device_config: TasmotaDeviceConfig,
) -> bool:
    """Log and create repairs issue if several devices share the same topic."""
    duplicated = False
    offenders = []
    for other_mac, other_config in hass.data[DISCOVERY_DATA].items():
        if own_mac and other_mac == own_mac:
            continue
        if command_topic == get_topic_command(other_config):
            offenders.append((other_mac, tasmota_get_device_config(other_config)))
    issue_id = f"topic_duplicated_{command_topic}"
    if offenders:
        if own_mac:
            offenders.append((own_mac, own_device_config))
        offender_strings = [
            f"'{cfg[tasmota_const.CONF_NAME]}' ({cfg[tasmota_const.CONF_IP]})"
            for _, cfg in offenders
        ]
        _LOGGER.warning(
            (
                "Multiple Tasmota devices are sharing the same topic '%s'. Offending"
                " devices: %s"
            ),
            command_topic,
            ", ".join(offender_strings),
        )
        ir.async_create_issue(
            hass,
            DOMAIN,
            issue_id,
            data={
                "key": "topic_duplicated",
                "mac": " ".join([mac for mac, _ in offenders]),
                "topic": command_topic,
            },
            is_fixable=False,
            learn_more_url=MQTT_TOPIC_URL,
            severity=ir.IssueSeverity.ERROR,
            translation_key="topic_duplicated",
            translation_placeholders={
                "topic": command_topic,
                "offenders": "\n\n* " + "\n\n* ".join(offender_strings),
            },
        )
        duplicated = True
    return duplicated


class DuplicatedTopicIssueData(TypedDict):
    """Typed result dict."""

    key: str
    mac: str
    topic: str


async def async_start(  # noqa: C901
    hass: HomeAssistant,
    discovery_topic: str,
    config_entry: ConfigEntry,
    tasmota_mqtt: TasmotaMQTTClient,
    setup_device: SetupDeviceCallback,
) -> None:
    """Start Tasmota device discovery."""

    def _discover_entity(
        tasmota_entity_config: TasmotaEntityConfig | None,
        discovery_hash: DiscoveryHashType,
        platform: str,
    ) -> None:
        """Handle adding or updating a discovered entity."""
        if not tasmota_entity_config:
            # Entity disabled, clean up entity registry
            entity_registry = er.async_get(hass)
            unique_id = unique_id_from_hash(discovery_hash)
            entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
            if entity_id:
                _LOGGER.debug("Removing entity: %s %s", platform, discovery_hash)
                entity_registry.async_remove(entity_id)
            return

        if discovery_hash in hass.data[ALREADY_DISCOVERED]:
            _LOGGER.debug(
                "Entity already added, sending update: %s %s",
                platform,
                discovery_hash,
            )
            async_dispatcher_send(
                hass,
                TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
                tasmota_entity_config,
            )
        else:
            tasmota_entity = tasmota_get_entity(tasmota_entity_config, tasmota_mqtt)
            if not tasmota_entity:
                _LOGGER.error("Failed to create entity %s %s", platform, discovery_hash)
                return

            _LOGGER.debug(
                "Adding new entity: %s %s %s",
                platform,
                discovery_hash,
                tasmota_entity.unique_id,
            )

            hass.data[ALREADY_DISCOVERED][discovery_hash] = None

            async_dispatcher_send(
                hass,
                TASMOTA_DISCOVERY_ENTITY_NEW.format(platform),
                tasmota_entity,
                discovery_hash,
            )

    async def async_device_discovered(payload: dict, mac: str) -> None:
        """Process the received message."""

        if ALREADY_DISCOVERED not in hass.data:
            # Discovery is shutting down
            return

        _LOGGER.debug("Received discovery data for tasmota device: %s", mac)
        tasmota_device_config = tasmota_get_device_config(payload)
        await setup_device(tasmota_device_config, mac)

        hass.data[DISCOVERY_DATA][mac] = payload

        add_entities = True

        command_topic = get_topic_command(payload) if payload else None
        state_topic = get_topic_stat(payload) if payload else None

        # Create or clear issue if topic is missing prefix
        issue_id = f"topic_no_prefix_{mac}"
        if payload and command_topic == state_topic:
            _LOGGER.warning(
                "Tasmota device '%s' with IP %s doesn't set %%prefix%% in its topic",
                tasmota_device_config[tasmota_const.CONF_NAME],
                tasmota_device_config[tasmota_const.CONF_IP],
            )
            ir.async_create_issue(
                hass,
                DOMAIN,
                issue_id,
                data={"key": "topic_no_prefix"},
                is_fixable=False,
                learn_more_url=MQTT_TOPIC_URL,
                severity=ir.IssueSeverity.ERROR,
                translation_key="topic_no_prefix",
                translation_placeholders={
                    "name": tasmota_device_config[tasmota_const.CONF_NAME],
                    "ip": tasmota_device_config[tasmota_const.CONF_IP],
                },
            )
            add_entities = False
        else:
            ir.async_delete_issue(hass, DOMAIN, issue_id)

        # Clear previous issues caused by duplicated topic
        issue_reg = ir.async_get(hass)
        tasmota_issues = [
            issue for key, issue in issue_reg.issues.items() if key[0] == DOMAIN
        ]
        for issue in tasmota_issues:
            if issue.data and issue.data["key"] == "topic_duplicated":
                issue_data: DuplicatedTopicIssueData = cast(
                    DuplicatedTopicIssueData, issue.data
                )
                macs = issue_data["mac"].split()
                if mac not in macs:
                    continue
                if payload and command_topic == issue_data["topic"]:
                    continue
                if len(macs) > 2:
                    # This device is no longer duplicated, update the issue
                    warn_if_topic_duplicated(hass, issue_data["topic"], None, {})
                    continue
                ir.async_delete_issue(hass, DOMAIN, issue.issue_id)

        if not payload:
            return

        # Warn and add issues if there are duplicated topics
        if warn_if_topic_duplicated(hass, command_topic, mac, tasmota_device_config):
            add_entities = False

        if not add_entities:
            # Add the device entry so the user can identify the device, but do not add
            # entities or triggers
            return

        tasmota_triggers = tasmota_get_triggers(payload)
        for trigger_config in tasmota_triggers:
            discovery_hash: DiscoveryHashType = (
                mac,
                "automation",
                "trigger",
                trigger_config.trigger_id,
            )
            if discovery_hash in hass.data[ALREADY_DISCOVERED]:
                _LOGGER.debug(
                    "Trigger already added, sending update: %s",
                    discovery_hash,
                )
                async_dispatcher_send(
                    hass,
                    TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
                    trigger_config,
                )
            elif trigger_config.is_active:
                _LOGGER.debug("Adding new trigger: %s", discovery_hash)
                hass.data[ALREADY_DISCOVERED][discovery_hash] = None

                tasmota_trigger = tasmota_get_trigger(trigger_config, tasmota_mqtt)

                async_dispatcher_send(
                    hass,
                    TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation"),
                    tasmota_trigger,
                    discovery_hash,
                )

        for platform in PLATFORMS:
            tasmota_entities = tasmota_get_entities_for_platform(payload, platform)
            for tasmota_entity_config, discovery_hash in tasmota_entities:
                _discover_entity(tasmota_entity_config, discovery_hash, platform)

    async def async_sensors_discovered(
        sensors: list[tuple[TasmotaBaseSensorConfig, DiscoveryHashType]], mac: str
    ) -> None:
        """Handle discovery of (additional) sensors."""
        platform = sensor.DOMAIN

        device_registry = dr.async_get(hass)
        entity_registry = er.async_get(hass)
        device = device_registry.async_get_device(
            set(), {(dr.CONNECTION_NETWORK_MAC, mac)}
        )

        if device is None:
            _LOGGER.warning("Got sensors for unknown device mac: %s", mac)
            return

        orphaned_entities = {
            entry.unique_id
            for entry in async_entries_for_device(
                entity_registry, device.id, include_disabled_entities=True
            )
            if entry.domain == sensor.DOMAIN and entry.platform == DOMAIN
        }
        for tasmota_sensor_config, discovery_hash in sensors:
            if tasmota_sensor_config:
                orphaned_entities.discard(tasmota_sensor_config.unique_id)
            _discover_entity(tasmota_sensor_config, discovery_hash, platform)
        for unique_id in orphaned_entities:
            entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
            if entity_id:
                _LOGGER.debug("Removing entity: %s %s", platform, entity_id)
                entity_registry.async_remove(entity_id)

    hass.data[ALREADY_DISCOVERED] = {}
    hass.data[DISCOVERY_DATA] = {}

    tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt)
    await tasmota_discovery.start_discovery(
        async_device_discovered, async_sensors_discovered
    )
    hass.data[TASMOTA_DISCOVERY_INSTANCE] = tasmota_discovery


async def async_stop(hass: HomeAssistant) -> None:
    """Stop Tasmota device discovery."""
    hass.data.pop(ALREADY_DISCOVERED)
    tasmota_discovery = hass.data.pop(TASMOTA_DISCOVERY_INSTANCE)
    await tasmota_discovery.stop_discovery()
