from collections import defaultdict
from collections.abc import Mapping
from copy import deepcopy
from dataclasses import dataclass
from enum import StrEnum
import logging
import os
from typing import Any, cast

from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
from homeassistant.components.lawn_mower import DOMAIN as LAWN_MOWER_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import translation
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType

from custom_components.powercalc.const import (
    BUILT_IN_LIBRARY_DIR,
    CONF_ENERGY_SENSOR_NAMING,
    CONF_MAX_POWER,
    CONF_MIN_POWER,
    CONF_POWER,
    CONF_POWER_SENSOR_NAMING,
    DEFAULT_SELF_USAGE_ENERGY_NAME_PATTERN,
    DEFAULT_SELF_USAGE_POWER_NAME_PATTERN,
    DOMAIN,
    CalculationStrategy,
    PowerProfileSource,
)
from custom_components.powercalc.errors import (
    ModelNotSupportedError,
    UnsupportedStrategyError,
)
from custom_components.powercalc.power_profile.sub_profile_selector import SubProfileSelectConfig

_LOGGER = logging.getLogger(__name__)


class DeviceType(StrEnum):
    CAMERA = "camera"
    COVER = "cover"
    FAN = "fan"
    GENERIC_IOT = "generic_iot"
    LIGHT = "light"
    POWER_METER = "power_meter"
    PRINTER = "printer"
    SMART_DIMMER = "smart_dimmer"
    SMART_SWITCH = "smart_switch"
    SMART_SPEAKER = "smart_speaker"
    TELEVISION = "television"
    NETWORK = "network"
    VACUUM_ROBOT = "vacuum_robot"
    LAWN_MOWER_ROBOT = "lawn_mower_robot"
    HEATING = "heating"
    UPS = "ups"


class DiscoveryBy(StrEnum):
    CONFIG_ENTRY = "config_entry"
    DEVICE = "device"
    ENTITY = "entity"


@dataclass(frozen=True)
class CustomField:
    key: str
    label: str
    selector: dict[str, Any]
    description: str | None = None
    default: Any = None


DEVICE_TYPE_DOMAIN: dict[DeviceType, str | set[str]] = {
    DeviceType.CAMERA: CAMERA_DOMAIN,
    DeviceType.COVER: COVER_DOMAIN,
    DeviceType.FAN: FAN_DOMAIN,
    DeviceType.GENERIC_IOT: {SENSOR_DOMAIN, MEDIA_PLAYER_DOMAIN},
    DeviceType.LIGHT: LIGHT_DOMAIN,
    DeviceType.POWER_METER: SENSOR_DOMAIN,
    DeviceType.SMART_DIMMER: LIGHT_DOMAIN,
    DeviceType.SMART_SWITCH: {SWITCH_DOMAIN, LIGHT_DOMAIN},
    DeviceType.SMART_SPEAKER: MEDIA_PLAYER_DOMAIN,
    DeviceType.TELEVISION: MEDIA_PLAYER_DOMAIN,
    DeviceType.NETWORK: BINARY_SENSOR_DOMAIN,
    DeviceType.PRINTER: SENSOR_DOMAIN,
    DeviceType.VACUUM_ROBOT: VACUUM_DOMAIN,
    DeviceType.LAWN_MOWER_ROBOT: LAWN_MOWER_DOMAIN,
    DeviceType.HEATING: CLIMATE_DOMAIN,
    DeviceType.UPS: SENSOR_DOMAIN,
}

SUPPORTED_DOMAINS: set[str] = {
    domain for domains in DEVICE_TYPE_DOMAIN.values() for domain in (domains if isinstance(domains, set) else {domains})
}


def _build_domain_device_type_mapping() -> Mapping[str, set[DeviceType]]:
    """Get the device types for a given entity domain."""
    domain_to_device_type: defaultdict[str, set[DeviceType]] = defaultdict(set)
    for device_type, domains in DEVICE_TYPE_DOMAIN.items():
        domain_set = domains if isinstance(domains, set) else {domains}
        for domain in domain_set:
            domain_to_device_type[domain].add(device_type)
    return domain_to_device_type


DOMAIN_DEVICE_TYPE_MAPPING: Mapping[str, set[DeviceType]] = _build_domain_device_type_mapping()


def is_device_type_supported_for_entity(device_type: DeviceType | None, entity_entry: RegistryEntry) -> bool:
    """Check whether a device type can be applied to a given entity.

    Kept module level so discovery can apply it to the device type from the library index,
    without having to build the full power profile first.
    """
    if device_type is None:
        return False

    # see https://github.com/bramstroker/homeassistant-powercalc/issues/2529
    if device_type == DeviceType.PRINTER and entity_entry.unit_of_measurement:
        return False

    return device_type in DOMAIN_DEVICE_TYPE_MAPPING[entity_entry.domain]


class PowerProfile:
    def __init__(
        self,
        hass: HomeAssistant,
        manufacturer: str,
        model: str,
        directory: str,
        json_data: ConfigType,
        sub_profiles: list[tuple[str, dict[str, Any]]] | None = None,
    ) -> None:
        self._manufacturer = manufacturer
        self._model = model.replace("#slash#", "/")
        self._hass = hass
        self._directory = directory
        self._base_json_data = deepcopy(json_data)
        self._json_data = deepcopy(json_data)
        self.sub_profile: str | None = None
        self._sub_profile_dir: str | None = None
        self._sub_profiles = sub_profiles or []

    def get_model_directory(self, root_only: bool = False) -> str:
        """Get the model directory containing the data files."""
        if root_only:
            return self._directory

        return self._sub_profile_dir or self._directory

    @property
    def manufacturer(self) -> str:
        """Get the manufacturer of this profile."""
        return self._manufacturer

    @property
    def model(self) -> str:
        """Get the model of this profile."""
        return self._model

    @property
    def unique_id(self) -> str:
        """Get the unique id of this profile."""
        return self._json_data.get("unique_id") or f"{self._manufacturer}_{self._model}"

    @property
    def name(self) -> str:
        """Get the name of this profile."""
        return self._json_data.get("name") or ""

    @property
    def json_data(self) -> ConfigType:
        """Get the raw json data."""
        return self._json_data

    @property
    def standby_power(self) -> float:
        """Get the standby power when the device is off."""
        return self._json_data.get("standby_power") or 0

    @property
    def standby_power_on(self) -> float:
        """Get the standby power (self usage) when the device is on."""
        standby_power_on = self._json_data.get("standby_power_on")
        if standby_power_on is None and self.only_self_usage:
            return self.standby_power
        return standby_power_on or 0

    @property
    def calculation_strategy(self) -> CalculationStrategy:
        """Get the calculation strategy this profile provides"""
        return CalculationStrategy(str(self._json_data.get("calculation_strategy", CalculationStrategy.LUT)))

    @property
    def linked_profile(self) -> str | None:
        """Get the linked profile."""
        return self._json_data.get("linked_profile", self._json_data.get("linked_lut"))

    @property
    def calculation_enabled_condition(self) -> str | None:
        """Get the condition to enable the calculation."""
        return self._json_data.get("calculation_enabled_condition")

    @property
    def aliases(self) -> list[str]:
        """Get a list of aliases for this model."""
        return self._json_data.get("aliases") or []

    @property
    def linear_config(self) -> ConfigType | None:
        """Get configuration to set up linear strategy."""
        config = self.get_strategy_config(CalculationStrategy.LINEAR)
        if config is None:
            return {CONF_MIN_POWER: 0, CONF_MAX_POWER: 0}
        return config

    @property
    def min_version(self) -> str | None:
        """Get the minimum required version for this profile."""
        return self._json_data.get("min_version")  # pragma: no cover

    @property
    def multi_switch_config(self) -> ConfigType | None:
        """Get configuration to set up multi_switch strategy."""
        return self.get_strategy_config(CalculationStrategy.MULTI_SWITCH)

    @property
    def fixed_config(self) -> ConfigType | None:
        """Get configuration to set up fixed strategy."""
        config = self.get_strategy_config(CalculationStrategy.FIXED)
        if config is None and self.standby_power_on:
            return {CONF_POWER: 0}
        return config

    @property
    def composite_config(self) -> list[ConfigType] | None:
        """Get configuration to set up composite strategy."""
        return cast(list[ConfigType], self._json_data.get("composite_config"))

    @property
    def playbook_config(self) -> ConfigType | None:
        """Get configuration to set up playbook strategy."""
        return self.get_strategy_config(CalculationStrategy.PLAYBOOK)

    def get_strategy_config(self, strategy: CalculationStrategy) -> ConfigType | None:
        """Get configuration for a certain strategy."""
        if not self.is_strategy_supported(strategy):
            raise UnsupportedStrategyError(
                f"Strategy {strategy} is not supported by model: {self._model}",
            )
        return self._json_data.get(f"{strategy}_config")

    @property
    def sensor_config(self) -> ConfigType:
        """Additional sensor configuration."""
        sensor_config = dict(self._json_data.get("sensor_config") or {})
        if self.only_self_usage and CONF_POWER_SENSOR_NAMING not in sensor_config:
            sensor_config[CONF_POWER_SENSOR_NAMING] = DEFAULT_SELF_USAGE_POWER_NAME_PATTERN
        if self.only_self_usage and CONF_ENERGY_SENSOR_NAMING not in sensor_config:
            sensor_config[CONF_ENERGY_SENSOR_NAMING] = DEFAULT_SELF_USAGE_ENERGY_NAME_PATTERN
        return sensor_config

    def is_strategy_supported(self, mode: CalculationStrategy) -> bool:
        """Whether a certain calculation strategy is supported by this profile."""
        return mode == self.calculation_strategy

    @property
    def needs_fixed_config(self) -> bool:
        """Used for smart switches which only provides standby power values.
        This indicates the user must supply the power values in the config flow.
        """
        if self.only_self_usage:
            return False

        return self.is_strategy_supported(
            CalculationStrategy.FIXED,
        ) and not self._json_data.get("fixed_config")

    @property
    def needs_linear_config(self) -> bool:
        """
        Used for smart dimmers. This indicates the user must supply the power values in the config flow.
        """
        if self.only_self_usage:
            return False

        return self.is_strategy_supported(
            CalculationStrategy.LINEAR,
        ) and not self._json_data.get("linear_config")

    @property
    def device_type(self) -> DeviceType | None:
        """Get the device type of this profile."""
        device_type = self._json_data.get("device_type")
        if not device_type:
            return DeviceType.LIGHT
        try:
            return DeviceType(device_type)
        except ValueError:
            _LOGGER.warning("Unknown device type: %s", device_type)
            return None

    @property
    def discovery_by(self) -> DiscoveryBy:
        return DiscoveryBy(self._json_data.get("discovery_by", DiscoveryBy.ENTITY))

    @property
    def only_self_usage(self) -> bool:
        """Whether this profile only provides self usage."""
        return bool(self._json_data.get("only_self_usage", False))

    @property
    def has_custom_fields(self) -> bool:
        """Whether this profile has custom fields."""
        return bool(self._json_data.get("fields"))

    @property
    def custom_fields(self) -> list[CustomField]:
        """Get the custom fields of this profile."""
        return [CustomField(key=key, **field) for key, field in self._json_data.get("fields", {}).items()]

    @property
    def documentation_url(self) -> str | None:
        """Get the documentation URL for this profile."""
        return self._json_data.get("documentation_url")

    @property
    def config_flow_discovery_remarks(self) -> str | None:
        """Get remarks to show at the config flow discovery step."""
        remarks = self._json_data.get("config_flow_discovery_remarks")
        if not remarks:
            translation_key = self.get_default_discovery_remarks_translation_key()
            if translation_key:
                translations = translation.async_get_cached_translations(
                    self._hass,
                    self._hass.config.language,
                    "common",
                    DOMAIN,
                )
                return translations.get(f"component.{DOMAIN}.common.{translation_key}")

        return remarks

    @property
    def config_flow_sub_profile_remarks(self) -> str | None:
        """Get extra remarks to show at the config flow sub profile step."""
        return self._json_data.get("config_flow_sub_profile_remarks")

    @property
    def compatible_integrations(self) -> list[str] | None:
        """Get the list of compatible integrations for this profile."""
        return self._json_data.get("compatible_integrations")

    def get_default_discovery_remarks_translation_key(self) -> str | None:
        """When no remarks are provided in the profile, see if we need to show a default remark."""
        if self.device_type == DeviceType.SMART_SWITCH and self.needs_fixed_config:
            return "remarks_smart_switch"
        if self.device_type == DeviceType.SMART_DIMMER and self.needs_linear_config:
            return "remarks_smart_dimmer"
        return None

    async def get_sub_profiles(self) -> list[tuple[str, dict[str, Any]]]:
        """Get listing of possible sub profiles and their corresponding JSON data."""
        return self._sub_profiles

    @property
    async def has_sub_profiles(self) -> bool:
        """Check whether this profile has sub profiles."""
        return len(await self.get_sub_profiles()) > 0

    @property
    async def requires_manual_sub_profile_selection(self) -> bool:
        """Check whether this profile requires manual sub profile selection."""
        if not await self.has_sub_profiles:
            return False

        return not self.has_sub_profile_select_matchers

    @property
    def sub_profile_select(self) -> SubProfileSelectConfig | None:
        """Get the configuration for automatic sub profile switching."""
        select_dict = self._json_data.get("sub_profile_select")
        if not select_dict:
            return None
        return SubProfileSelectConfig(**select_dict)

    @property
    def has_sub_profile_select_matchers(self) -> bool:
        """Check whether the sub profile select has matchers."""
        if not self.sub_profile_select:
            return False
        return bool(self.sub_profile_select.matchers)

    async def select_sub_profile(self, sub_profile: str) -> None:
        """Select a sub profile. Only applicable when to profile actually supports sub profiles."""
        if not await self.has_sub_profiles:
            return

        # Sub profile already selected, no need to load it again
        if self.sub_profile == sub_profile:
            return

        sub_profiles = await self.get_sub_profiles()
        found_profile = None
        for sub_dir, json_data in sub_profiles:
            if sub_dir == sub_profile:
                found_profile = json_data
                break

        if found_profile is None:
            raise ModelNotSupportedError(
                f"Sub profile not found (manufacturer: {self._manufacturer}, "
                f"model: {self._model}, sub_profile: {sub_profile})",
            )

        self._sub_profile_dir = os.path.join(self._directory, sub_profile)
        _LOGGER.debug("Loading sub profile: %s", sub_profile)

        self._json_data = deepcopy(self._base_json_data)
        self._json_data.update(deepcopy(found_profile))

        self.sub_profile = sub_profile

    @property
    async def needs_user_configuration(self) -> bool:
        """Check whether this profile needs user configuration."""
        if self.calculation_strategy == CalculationStrategy.MULTI_SWITCH:
            return True

        if self.needs_fixed_config or self.needs_linear_config:
            return True

        if self.has_custom_fields:
            return True

        return await self.has_sub_profiles and not self.sub_profile_select

    def is_entity_domain_supported(self, entity_entry: RegistryEntry) -> bool:
        """Check whether this power profile supports a given entity domain."""
        return is_device_type_supported_for_entity(self.device_type, entity_entry)

    @property
    def is_custom_profile(self) -> bool:
        """Whether this profile is a custom profile."""
        return not self._directory.startswith(self._hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR))

    @property
    def configuration_source(self) -> PowerProfileSource:
        return PowerProfileSource.LIBRARY_CUSTOM if self.is_custom_profile else PowerProfileSource.LIBRARY_BUILTIN
