"""Common models for HomeWizard Energy API."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from typing import Any

from awesomeversion import AwesomeVersion
from mashumaro.config import BaseConfig
from mashumaro.exceptions import MissingField
from mashumaro.mixins.orjson import DataClassORJSONMixin
from mashumaro.types import SerializationStrategy

from .const import LOGGER, MODEL_TO_ID, MODEL_TO_NAME, Model
from .utils import get_awesome_version


class AwesomeVersionSerializationStrategy(SerializationStrategy, use_annotations=True):
    """Serialization strategy for AwesomeVersion objects."""

    def serialize(self, value: AwesomeVersion) -> str:
        """Serialize AwesomeVersion object to string."""
        return str(value)  # pragma: no cover

    def deserialize(self, value: str) -> AwesomeVersion | None:
        """Deserialize string to AwesomeVersion object."""
        version = get_awesome_version(value)
        return version


class BaseModel(DataClassORJSONMixin):
    """Base model for all HomeWizard models."""

    # pylint: disable-next=too-few-public-methods
    class Config(BaseConfig):
        """Mashumaro configuration."""

        serialize_by_alias = True
        serialization_strategy = {  # noqa: RUF012
            AwesomeVersion: AwesomeVersionSerializationStrategy()
        }
        omit_none = True


class UpdateBaseModel(BaseModel):
    """Base model for all 'update' models."""

    def __post_serialize__(self, d: dict, context: dict | None = None):
        """Post serialize hook for UpdateBaseModel object."""
        _ = context  # Unused

        if not d:
            raise ValueError("No values to update")

        return d


@dataclass(kw_only=True)
class CombinedModels:
    """All values."""

    device: Device
    measurement: Measurement
    state: State | None
    system: System | None
    batteries: Batteries | None

    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-positional-arguments
    def __init__(
        self,
        device: Device,
        measurement: Measurement,
        state: State | None,
        system: System | None,
        batteries: Batteries | None = None,
    ):
        self.device = device
        self.measurement = measurement
        self.state = state
        self.system = system
        self.batteries = batteries

        # Move things around for backwards compatibility
        ## measurement.wifi_ssid -> system.wifi_ssid
        if self.measurement is not None and self.measurement.wifi_ssid is not None:
            if self.system is None:
                self.system = System()
            self.system.wifi_ssid = self.measurement.wifi_ssid

        ## measurement.wifi_strength -> system.wifi_strength_pct
        if self.measurement is not None and self.measurement.wifi_strength is not None:
            if self.system is None:
                self.system = System()
            self.system.wifi_strength_pct = self.measurement.wifi_strength

        ## state.brightness -> system.status_led_brightness_pct
        if self.state is not None and self.state.brightness is not None:
            if self.system is None:
                self.system = System()
            self.system.status_led_brightness_pct = (self.state.brightness / 255) * 100


def get_verification_hostname(model: str, serial_number: str) -> str:
    """Helper method to convert device model and serial to identifier

    The identifier is used to verify the device in the HomeWizard Energy API via HTTPS.
    """

    if model not in MODEL_TO_ID:
        raise ValueError(f"Unsupported model: {model}")

    return f"appliance/{MODEL_TO_ID[model]}/{serial_number}"


@dataclass(kw_only=True)
class Device(BaseModel):
    """Represent Device config."""

    model_name: str | None = None
    id: str | None = None

    product_name: str = field()
    product_type: str = field()
    serial: str = field()
    api_version: AwesomeVersion = field()
    firmware_version: str = field()

    @classmethod
    def __post_deserialize__(cls, obj: Device) -> Device:
        """Post deserialize hook for Device object."""
        _ = cls  # Unused

        obj.model_name = MODEL_TO_NAME.get(obj.product_type)
        obj.id = get_verification_hostname(obj.product_type, obj.serial)
        return obj

    def supports_state(self) -> bool:
        """Return if the device supports state."""
        return self.product_type == Model.ENERGY_SOCKET

    def supports_led_brightness(self) -> bool:
        """Return if the device supports LED brightness control."""
        if self.product_type in (
            Model.BATTERY,
            Model.ENERGY_SOCKET,
        ):
            return True

        # P1 Meter supports LED brightness from API v2.0.0 onwards
        if self.product_type == Model.P1_METER:
            return self.api_version.major >= 2

        return False

    def supports_cloud_enable(self) -> bool:
        """Return if the device supports cloud enable/disable."""
        return self.product_type != Model.BATTERY

    def supports_reboot(self) -> bool:
        """Return if the device supports reboot."""
        return self.api_version.major >= 2 and self.product_type != Model.BATTERY

    def supports_identify(self) -> bool:
        """Return if the device supports identify."""
        return self.product_type not in (
            Model.ENERGY_METER_1_PHASE,
            Model.ENERGY_METER_3_PHASE,
            Model.ENERGY_METER_EASTRON_SDM230,
            Model.ENERGY_METER_EASTRON_SDM630,
        )

    def supports_telegram(self) -> bool:
        """Return if the device supports telegram."""
        return self.product_type == Model.P1_METER

    def supports_batteries(self) -> bool:
        """Return if the device supports batteries."""
        return self.product_type in (
            Model.P1_METER,
            Model.ENERGY_METER_1_PHASE,
            Model.ENERGY_METER_3_PHASE,
            Model.ENERGY_METER_EASTRON_SDM230,
            Model.ENERGY_METER_EASTRON_SDM630,
        )

    def supported_battery_modes(self) -> list[Batteries.Mode] | None:
        """Return list of supported battery modes."""
        if not self.supports_batteries():
            return None

        modes = [
            Batteries.Mode.STANDBY,
            Batteries.Mode.TO_FULL,
            Batteries.Mode.ZERO,
        ]

        if self.api_version >= AwesomeVersion("2.2.0"):
            modes += [
                Batteries.Mode.ZERO_CHARGE_ONLY,
                Batteries.Mode.ZERO_DISCHARGE_ONLY,
            ]

        return modes


@dataclass(kw_only=True)
class Measurement(BaseModel):
    """Represent Measurement."""

    # Deprecated, use 'System'
    wifi_ssid: str | None = field(
        default=None,
    )
    wifi_strength: int | None = field(
        default=None,
    )

    # Generic
    energy_import_kwh: float | None = field(
        default=None,
    )
    energy_import_t1_kwh: float | None = field(
        default=None,
    )
    energy_import_t2_kwh: float | None = field(
        default=None,
    )
    energy_import_t3_kwh: float | None = field(
        default=None,
    )
    energy_import_t4_kwh: float | None = field(
        default=None,
    )
    energy_export_kwh: float | None = field(
        default=None,
    )
    energy_export_t1_kwh: float | None = field(
        default=None,
    )
    energy_export_t2_kwh: float | None = field(
        default=None,
    )
    energy_export_t3_kwh: float | None = field(
        default=None,
    )
    energy_export_t4_kwh: float | None = field(
        default=None,
    )

    power_w: float | None = field(
        default=None,
    )
    power_l1_w: float | None = field(
        default=None,
    )
    power_l2_w: float | None = field(
        default=None,
    )
    power_l3_w: float | None = field(
        default=None,
    )

    voltage_v: float | None = field(
        default=None,
    )
    voltage_l1_v: float | None = field(
        default=None,
    )
    voltage_l2_v: float | None = field(
        default=None,
    )
    voltage_l3_v: float | None = field(
        default=None,
    )

    current_a: float | None = field(
        default=None,
    )
    current_l1_a: float | None = field(
        default=None,
    )
    current_l2_a: float | None = field(
        default=None,
    )
    current_l3_a: float | None = field(
        default=None,
    )

    apparent_power_va: float | None = field(
        default=None,
    )
    apparent_power_l1_va: float | None = field(
        default=None,
    )
    apparent_power_l2_va: float | None = field(
        default=None,
    )
    apparent_power_l3_va: float | None = field(
        default=None,
    )

    reactive_power_var: float | None = field(
        default=None,
    )
    reactive_power_l1_var: float | None = field(
        default=None,
    )
    reactive_power_l2_var: float | None = field(
        default=None,
    )
    reactive_power_l3_var: float | None = field(
        default=None,
    )

    power_factor: float | None = field(
        default=None,
    )
    power_factor_l1: float | None = field(
        default=None,
    )
    power_factor_l2: float | None = field(
        default=None,
    )
    power_factor_l3: float | None = field(
        default=None,
    )

    frequency_hz: float | None = field(
        default=None,
    )

    # P1 Meter specific
    timestamp: datetime | None = field(
        default=None,
        metadata={"deserialize": lambda x: Measurement.to_datetime(x)},
    )
    protocol_version: int | None = field(
        default=None,
    )
    meter_model: str | None = field(
        default=None,
    )
    unique_id: str | None = field(
        default=None,
        metadata={"deserialize": lambda x: Measurement.hex_to_readable(x)},
    )

    tariff: int | None = field(
        default=None,
    )

    voltage_sag_l1_count: int | None = field(
        default=None,
    )
    voltage_sag_l2_count: int | None = field(
        default=None,
    )
    voltage_sag_l3_count: int | None = field(
        default=None,
    )

    voltage_swell_l1_count: int | None = field(
        default=None,
    )
    voltage_swell_l2_count: int | None = field(
        default=None,
    )
    voltage_swell_l3_count: int | None = field(
        default=None,
    )

    any_power_fail_count: int | None = field(
        default=None,
    )
    long_power_fail_count: int | None = field(
        default=None,
    )

    average_power_15m_w: float | None = field(
        default=None,
    )
    monthly_power_peak_w: float | None = field(
        default=None,
    )
    monthly_power_peak_timestamp: datetime | None = field(
        default=None,
        metadata={"deserialize": lambda x: Measurement.to_datetime(x)},
    )

    external_devices: dict[str, ExternalDevice] | None = field(
        default=None,
        metadata={
            "alias": "external",
            "deserialize": lambda list: Measurement.array_to_external_device_list(list),
        },
    )

    # Watermeter Specific
    active_liter_lpm: float | None = field(
        default=None,
    )
    total_liter_m3: float | None = field(
        default=None,
    )

    # Battery Specific
    cycles: int | None = field(
        default=None,
    )
    state_of_charge_pct: float | None = field(
        default=None,
    )

    @staticmethod
    def array_to_external_device_list(devices: list[dict]) -> dict[str, ExternalDevice]:
        """Convert external device dict to list of ExternalDevice objects."""
        rv: dict[str, ExternalDevice] = {}

        for item in devices:
            try:
                device = ExternalDevice.from_dict(item)
            except MissingField as e:
                LOGGER.error("Error converting external device: %s", e)
                continue
            rv[f"{device.type}_{device.unique_id}"] = device

        return rv

    @staticmethod
    def to_datetime(timestamp: str | int) -> datetime:
        """Convert DSRM gas-timestamp to datetime object.

        Args:
            timestamp: Timestamp string, formatted as YYMMDDHHMMSS or YYYY-MM-DDTHH:MM:SS

        Returns:
            A datetime object.
        """
        if isinstance(timestamp, int):
            # V1 API uses int for timestamp
            return datetime.strptime(str(timestamp), "%y%m%d%H%M%S")

        return datetime.fromisoformat(timestamp)

    @staticmethod
    def hex_to_readable(value: str | None) -> str | None:
        """Convert hex string to readable string, if possible.

        Args:
            value: String to convert, e.g. '4E47475955'

        Returns:
            A string formatted or original value when failed.
        """
        try:
            return bytes.fromhex(value).decode("utf-8")
        except (TypeError, ValueError):
            return value

    @classmethod
    # pylint: disable=too-many-statements
    def __pre_deserialize__(cls, d: dict[Any, Any]) -> dict[Any, Any]:
        _ = cls  # Unused

        if "wifi_ssid" not in d:
            # This is a v2 API response, no need to remap
            return d

        d["protocol_version"] = d.get("smr_version")
        d["tariff"] = d.get("active_tariff")
        d["energy_import_kwh"] = d.get(
            "total_power_import_kwh", d.get("total_power_import_t1_kwh")
        )
        d["energy_import_t1_kwh"] = d.get("total_power_import_t1_kwh")
        d["energy_import_t2_kwh"] = d.get("total_power_import_t2_kwh")
        d["energy_import_t3_kwh"] = d.get("total_power_import_t3_kwh")
        d["energy_import_t4_kwh"] = d.get("total_power_import_t4_kwh")
        d["energy_export_kwh"] = d.get(
            "total_power_export_kwh", d.get("total_power_export_t1_kwh")
        )
        d["energy_export_t1_kwh"] = d.get("total_power_export_t1_kwh")
        d["energy_export_t2_kwh"] = d.get("total_power_export_t2_kwh")
        d["energy_export_t3_kwh"] = d.get("total_power_export_t3_kwh")
        d["energy_export_t4_kwh"] = d.get("total_power_export_t4_kwh")
        d["power_w"] = d.get("active_power_w")
        d["power_l1_w"] = d.get("active_power_l1_w")
        d["power_l2_w"] = d.get("active_power_l2_w")
        d["power_l3_w"] = d.get("active_power_l3_w")
        d["voltage_v"] = d.get("active_voltage_v")
        d["voltage_l1_v"] = d.get("active_voltage_l1_v")
        d["voltage_l2_v"] = d.get("active_voltage_l2_v")
        d["voltage_l3_v"] = d.get("active_voltage_l3_v")
        d["current_a"] = d.get("active_current_a")
        d["current_l1_a"] = d.get("active_current_l1_a")
        d["current_l2_a"] = d.get("active_current_l2_a")
        d["current_l3_a"] = d.get("active_current_l3_a")
        d["apparent_power_va"] = d.get("active_apparent_power_va")
        d["apparent_power_l1_va"] = d.get("active_apparent_power_l1_va")
        d["apparent_power_l2_va"] = d.get("active_apparent_power_l2_va")
        d["apparent_power_l3_va"] = d.get("active_apparent_power_l3_va")
        d["reactive_power_var"] = d.get("active_reactive_power_var")
        d["reactive_power_l1_var"] = d.get("active_reactive_power_l1_var")
        d["reactive_power_l2_var"] = d.get("active_reactive_power_l2_var")
        d["reactive_power_l3_var"] = d.get("active_reactive_power_l3_var")
        d["power_factor"] = d.get("active_power_factor")
        d["power_factor_l1"] = d.get("active_power_factor_l1")
        d["power_factor_l2"] = d.get("active_power_factor_l2")
        d["power_factor_l3"] = d.get("active_power_factor_l3")
        d["frequency_hz"] = d.get("active_frequency_hz")
        d["average_power_15m_w"] = d.get("active_power_average_w")
        d["monthly_power_peak_w"] = d.get("montly_power_peak_w")
        d["monthly_power_peak_timestamp"] = d.get("montly_power_peak_timestamp")
        d["external_devices"] = d.get("external_devices")

        return d

    @classmethod
    def __post_deserialize__(cls, obj: Measurement) -> Measurement:
        """Post deserialize hook for Measurement object."""
        _ = cls  # Unused

        # Some smart meters report a tariff other than 1, 2, 3 or 4, which is invalid
        if obj.tariff not in (1, 2, 3, 4):
            obj.tariff = None

        return obj


@dataclass(kw_only=True)
class ExternalDevice(BaseModel):
    """Represents externally connected device."""

    class DeviceType(StrEnum):
        """Device type allocations."""

        GAS_METER = "gas_meter"
        HEAT_METER = "heat_meter"
        WARM_WATER_METER = "warm_water_meter"
        WATER_METER = "water_meter"
        INLET_HEAT_METER = "inlet_heat_meter"

    unique_id: datetime = field(
        metadata={"deserialize": lambda x: Measurement.hex_to_readable(x)}
    )

    type: DeviceType | None = field(
        default=None,
        metadata={
            "deserialize": lambda x: ExternalDevice.DeviceType.__members__.get(
                x.upper(), None
            )
        },
    )
    value: float = field()
    unit: str = field()
    timestamp: datetime = field(
        metadata={"deserialize": lambda x: Measurement.to_datetime(x)}
    )


@dataclass(kw_only=True)
class StateUpdate(UpdateBaseModel):
    """Represent State update config."""

    power_on: bool | None = field(default=None)
    switch_lock: bool | None = field(default=None)
    brightness: int | None = field(default=None)


@dataclass(kw_only=True)
class State(BaseModel):
    """Represent current state."""

    power_on: bool | None = field(
        default=None,
    )
    switch_lock: bool | None = field(
        default=None,
    )
    brightness: int | None = field(
        default=None,
    )


@dataclass
class SystemUpdate(UpdateBaseModel):
    """Represent System update config."""

    cloud_enabled: bool | None = field(default=None)
    status_led_brightness_pct: int | None = field(default=None)
    api_v1_enabled: bool | None = field(default=None)


@dataclass(kw_only=True)
class System(BaseModel):
    """Represent System config."""

    wifi_strength_pct: int | None = None

    wifi_ssid: str | None = field(default=None)
    wifi_rssi_db: int | None = field(default=None)
    cloud_enabled: bool | None = field(default=None)
    uptime_s: int | None = field(default=None)
    status_led_brightness_pct: int | None = field(default=None)
    api_v1_enabled: bool | None = field(default=None)

    @classmethod
    def __post_deserialize__(cls, obj: System) -> System:
        _ = cls  # Unused

        if obj.wifi_rssi_db is not None:
            obj.wifi_strength_pct = (
                0
                if obj.wifi_rssi_db <= -100 or obj.wifi_rssi_db == 0
                else 100
                if obj.wifi_rssi_db >= -50
                else 2 * (obj.wifi_rssi_db + 100)
            )

        return obj


@dataclass
class BatteriesUpdate(UpdateBaseModel):
    """Represent Batteries update config."""

    mode: Batteries.Mode | None = field(default=None)
    permissions: list[Batteries.Permissions] | None = field(default=None)

    @staticmethod
    def from_mode(
        mode: Batteries.Mode,
    ) -> BatteriesUpdate:
        """Convert userfacing mode to API mode.

        Args:
            mode: Userfacing mode to convert.
        Returns:
            BatteriesUpdate object with API mode and permissions.
        """

        match mode:
            case Batteries.Mode.ZERO:
                return BatteriesUpdate(
                    mode=Batteries.Mode.ZERO,
                    permissions=[
                        Batteries.Permissions.CHARGE_ALLOWED,
                        Batteries.Permissions.DISCHARGE_ALLOWED,
                    ],
                )
            case Batteries.Mode.ZERO_CHARGE_ONLY:
                return BatteriesUpdate(
                    mode=Batteries.Mode.ZERO,
                    permissions=[
                        Batteries.Permissions.CHARGE_ALLOWED,
                    ],
                )
            case Batteries.Mode.ZERO_DISCHARGE_ONLY:
                return BatteriesUpdate(
                    mode=Batteries.Mode.ZERO,
                    permissions=[
                        Batteries.Permissions.DISCHARGE_ALLOWED,
                    ],
                )
            case Batteries.Mode.TO_FULL:
                return BatteriesUpdate(mode=Batteries.Mode.TO_FULL)
            case Batteries.Mode.STANDBY:
                return BatteriesUpdate(mode=Batteries.Mode.STANDBY, permissions=[])

        raise ValueError(f"Unsupported Batteries.Mode: {mode!r}")


@dataclass(kw_only=True)
class Batteries(BaseModel):
    """Represent Batteries config."""

    class Mode(StrEnum):
        """Device type allocations.

        This is the list of modes shown to the user. 'zero_charge_only' and 'zero_discharge_only' are
        mapped to 'zero' in the API via the Permissions parameter
        """

        ZERO = "zero"
        TO_FULL = "to_full"
        STANDBY = "standby"
        ZERO_CHARGE_ONLY = "zero_charge_only"
        ZERO_DISCHARGE_ONLY = "zero_discharge_only"

    class Permissions(StrEnum):
        """Device permission allocations."""

        CHARGE_ALLOWED = "charge_allowed"
        DISCHARGE_ALLOWED = "discharge_allowed"

    mode: Mode = field(
        metadata={
            "deserialize": lambda x: Batteries.Mode.__members__.get(x.upper(), None)
        },
    )
    permissions: list[Permissions] | None = field(
        default=None,
        metadata={
            "deserialize": lambda lst: [
                perm
                for item in lst
                if (perm := Batteries.Permissions.__members__.get(item.upper(), None))
                is not None
            ]
            if lst is not None
            else None
        },
    )
    power_w: float = field()
    target_power_w: float = field()
    max_consumption_w: float = field()
    max_production_w: float = field()
    battery_count: int | None = field(default=None)

    @classmethod
    def __post_deserialize__(cls, obj: Batteries) -> Batteries:
        """Set correct mode based on permissions after deserialization.
        If permissions is missing and mode is ZERO, keep mode as ZERO for backwards compatibility.
        If permissions is present (even if empty), apply the mapping logic.
        """
        # Only adjust if mode is ZERO
        if obj.mode != cls.Mode.ZERO:
            return obj

        # Detect if 'permissions' was present in the original data
        # If using mashumaro, the original dict is not available here, so we infer:
        # If permissions is None, treat as 'not provided'. If it's an empty list, treat as 'provided but empty'.
        if obj.permissions is None:
            # Permissions not provided, keep mode as ZERO (backwards compatibility)
            return obj

        perms = set(obj.permissions)
        if perms == {cls.Permissions.CHARGE_ALLOWED}:
            obj.mode = cls.Mode.ZERO_CHARGE_ONLY
        elif perms == {cls.Permissions.DISCHARGE_ALLOWED}:
            obj.mode = cls.Mode.ZERO_DISCHARGE_ONLY
        elif perms == {
            cls.Permissions.CHARGE_ALLOWED,
            cls.Permissions.DISCHARGE_ALLOWED,
        }:
            obj.mode = cls.Mode.ZERO
        elif perms == set():
            obj.mode = cls.Mode.STANDBY

        return obj


@dataclass(kw_only=True)
class Token(BaseModel):
    """Represent Token."""

    token: str = field()
