from __future__ import annotations

import datetime
import logging
from collections.abc import Iterator
from dataclasses import dataclass, field
from enum import Enum
from typing import TypeVar

from dacite import Config, from_dict

from myPyllant.utils import datetime_parse

logger = logging.getLogger(__name__)


class MyPyllantEnum(Enum):
    def __str__(self):
        """
        Return 'HOUR' instead of 'DeviceDataBucketResolution.HOUR'
        """
        return self.value

    @property
    def display_value(self) -> str:
        return self.value.replace("_", " ").title()


class CircuitState(MyPyllantEnum):
    HEATING = "HEATING"
    COOLING = "COOLING"
    STANDBY = "STANDBY"


class DeviceDataBucketResolution(MyPyllantEnum):
    HOUR = "HOUR"
    DAY = "DAY"
    MONTH = "MONTH"


class ZoneHeatingOperatingMode(MyPyllantEnum):
    MANUAL = "MANUAL"
    TIME_CONTROLLED = "TIME_CONTROLLED"
    OFF = "OFF"


class ZoneCurrentSpecialFunction(MyPyllantEnum):
    NONE = "NONE"
    QUICK_VETO = "QUICK_VETO"
    HOLIDAY = "HOLIDAY"
    SYSTEM_OFF = "SYSTEM_OFF"


class ZoneHeatingState(MyPyllantEnum):
    IDLE = "IDLE"
    HEATING_UP = "HEATING_UP"
    COOLING_DOWN = "COOLING_DOWN"


class DHWCurrentSpecialFunction(MyPyllantEnum):
    CYLINDER_BOOST = "CYLINDER_BOOST"
    REGULAR = "REGULAR"


class DHWOperationMode(MyPyllantEnum):
    MANUAL = "MANUAL"
    TIME_CONTROLLED = "TIME_CONTROLLED"
    OFF = "OFF"


T = TypeVar("T")


@dataclass
class MyPyllantDataClass:
    """
    Base class that runs type validation in __init__ and can create an instance from API values
    """

    @classmethod
    def from_api(cls: type[T], **kwargs) -> T:
        """
        Creates enums & dates from strings before calling __init__
        """
        return from_dict(
            data_class=cls,
            data=kwargs,
            config=Config(cast=[Enum], type_hooks={datetime.datetime: datetime_parse}),
        )


@dataclass
class Claim(MyPyllantDataClass):
    country_code: str
    nomenclature: str
    serial_number: str
    state: str
    system_id: str
    home_name: str | None = None
    address: str | None = None
    product_information: str | None = None
    migration_state: str | None = None
    cag: bool | None = None
    firmware_version: str | None = None
    firmware: dict | None = field(default_factory=dict)
    product_metadata: dict = field(default_factory=dict)


@dataclass
class ZoneHeating(MyPyllantDataClass):
    manual_mode_setpoint_heating: float
    operation_mode_heating: ZoneHeatingOperatingMode
    time_program_heating: dict
    set_back_temperature: float


@dataclass
class Zone(MyPyllantDataClass):
    system_id: str
    general: dict
    index: int
    is_active: bool
    is_cooling_allowed: bool
    zone_binding: str
    heating_state: ZoneHeatingState
    heating: ZoneHeating
    current_special_function: ZoneCurrentSpecialFunction
    current_room_temperature: float | None = None
    desired_room_temperature_setpoint_heating: float | None = None
    desired_room_temperature_setpoint_cooling: float | None = None
    desired_room_temperature_setpoint: float | None = None
    current_room_humidity: float | None = None
    associated_circuit_index: int | None = None
    quick_veto_start_date_time: datetime.datetime | None = None
    quick_veto_end_date_time: datetime.datetime | None = None

    @classmethod
    def from_api(cls, **kwargs):
        kwargs["heating"] = ZoneHeating.from_api(**kwargs["heating"])
        return super().from_api(**kwargs)

    @property
    def name(self):
        return self.general["name"]


@dataclass
class Circuit(MyPyllantDataClass):
    system_id: str
    index: int
    circuit_state: CircuitState
    is_cooling_allowed: bool
    mixer_circuit_type_external: str
    set_back_mode_enabled: bool
    zones: list = field(default_factory=list)
    current_circuit_flow_temperature: float | None = None
    heating_curve: float | None = None
    heating_flow_temperature_minimum_setpoint: float | None = None
    heating_flow_temperature_maximum_setpoint: float | None = None
    min_flow_temperature_setpoint: float | None = None
    calculated_energy_manager_state: str | None = None


@dataclass
class DomesticHotWater(MyPyllantDataClass):
    system_id: str
    index: int
    current_special_function: DHWCurrentSpecialFunction
    max_setpoint: float
    min_setpoint: float
    operation_mode_dhw: DHWOperationMode
    time_program_dhw: dict
    time_program_circulation_pump: dict
    current_dhw_temperature: float | None = None
    tapping_setpoint: float | None = None


@dataclass
class System(MyPyllantDataClass):
    id: str
    state: dict
    properties: dict
    configuration: dict
    claim: Claim | None = None
    timezone: datetime.tzinfo | None = None
    connected: bool | None = None
    diagnostic_trouble_codes: list[dict] | None = None
    current_system: dict = field(default_factory=dict)
    zones: list[Zone] = field(default_factory=list)
    circuits: list[Circuit] = field(default_factory=list)
    domestic_hot_water: list[DomesticHotWater] = field(default_factory=list)
    devices: list[Device] = field(default_factory=list)

    @classmethod
    def from_api(cls, **kwargs):
        if "claim" in kwargs and "id" not in kwargs:
            kwargs["id"] = kwargs["claim"].system_id
        system: System = super().from_api(**kwargs)
        logger.debug(f"Creating related models from state: {kwargs}")
        system.zones = [
            Zone.from_api(system_id=system.id, **z)
            for z in system.merge_object("zones")
        ]
        system.circuits = [
            Circuit.from_api(system_id=system.id, **c)
            for c in system.merge_object("circuits")
        ]
        system.domestic_hot_water = [
            DomesticHotWater.from_api(system_id=system.id, **d)
            for d in system.merge_object("dhw")
        ]
        system.devices = [
            Device.from_api(system_id=system.id, type=k, **v)
            for k, v in system.raw_devices
        ]
        return system

    @property
    def raw_devices(self) -> Iterator[tuple[str, dict]]:
        for key, device in self.current_system.items():
            if isinstance(device, dict) and "device_uuid" in device:
                yield key, device

    @property
    def primary_heat_generator(self) -> Device | None:
        devices = [d for d in self.devices if d.type == "primary_heat_generator"]
        if len(devices) > 0:
            return devices[0]
        return None

    def merge_object(self, obj_name) -> Iterator[dict]:
        """
        The Vaillant API returns information about zones, circuits, and dhw separately as
        configuration, state, and properties.

        This function merges everything together into one big dict for a given object (i.e. zones)
        """
        indexes = [o["index"] for o in self.configuration.get(obj_name, [])]
        for idx in indexes:
            # State and properties get merged into configuration
            configuration = next(
                c for c in self.configuration.get(obj_name, []) if c["index"] == idx
            )
            try:
                state = next(
                    c for c in self.state.get(obj_name, []) if c["index"] == idx
                )
            except (StopIteration, KeyError) as e:
                logger.warning("Error when merging state", exc_info=e)
                state = {}
            try:
                properties = next(
                    c for c in self.properties.get(obj_name, []) if c["index"] == idx
                )
            except (StopIteration, KeyError) as e:
                logger.warning("Error when merging properties", exc_info=e)
                properties = {}
            # index is part of all three fields, delete from two to avoid multiple keyword argument error in dict init
            configuration.update(state)
            configuration.update(properties)
            yield configuration

    @property
    def outdoor_temperature(self) -> float | None:
        try:
            return self.state["system"]["outdoor_temperature"]
        except KeyError:
            logger.info(
                "Could not get outdoor temperature from system control state",
            )
            return None

    @property
    def water_pressure(self) -> float | None:
        try:
            return self.state["system"]["system_water_pressure"]
        except KeyError:
            logger.info("Could not get water pressure from system control state")
            return None

    @property
    def system_name(self) -> str:
        if self.primary_heat_generator:
            return self.primary_heat_generator.product_name_display
        elif self.claim:
            return self.claim.nomenclature
        else:
            return "System"


@dataclass
class Device(MyPyllantDataClass):
    system_id: str
    device_uuid: str
    ebus_id: str
    article_number: str
    device_serial_number: str
    type: str
    device_type: str
    first_data: datetime.datetime
    last_data: datetime.datetime
    name: str | None = None
    product_name: str | None = None
    spn: int | None = None
    bus_coupler_address: int | None = None
    emf_valid: bool | None = None
    operational_data: dict = field(default_factory=dict)
    data: list[DeviceData] = field(default_factory=list)
    properties: list = field(default_factory=list)

    @property
    def name_display(self) -> str:
        """
        Product name might be empty, fall back to title-cased device type
        """
        return self.name or self.product_name_display

    @property
    def product_name_display(self) -> str:
        """
        Product name might be None, fall back to title-cased device type
        """
        return self.product_name or self.device_type.replace("_", "").title()

    @classmethod
    def from_api(cls, **kwargs):
        kwargs["data"] = (
            [DeviceData.from_api(**dd) for dd in kwargs["data"]]
            if "data" in kwargs
            else []
        )
        return super().from_api(**kwargs)


@dataclass
class DeviceDataBucket(MyPyllantDataClass):
    start_date: datetime.datetime
    end_date: datetime.datetime
    value: float | None = None


@dataclass
class DeviceData(MyPyllantDataClass):
    operation_mode: str
    device: Device | None = None
    data_from: datetime.datetime | None = None
    data_to: datetime.datetime | None = None
    start_date: datetime.datetime | None = None
    end_date: datetime.datetime | None = None
    resolution: DeviceDataBucketResolution | None = None
    energy_type: str | None = None
    value_type: str | None = None
    calculated: bool | None = None
    data: list[DeviceDataBucket] = field(default_factory=list)

    @classmethod
    def from_api(cls, **kwargs):
        kwargs["data_from"] = kwargs.pop("from", None)
        kwargs["data_to"] = kwargs.pop("to", None)
        kwargs["data"] = [
            DeviceDataBucket.from_api(**dd) for dd in kwargs.pop("data", [])
        ]
        return super().from_api(**kwargs)
