"""Endpoint object to access the endpoint settings/info"""
from __future__ import annotations

import json
import logging
from typing import Any

from .common.const import IGNORE_DEVICE
from .common.rest_client import RestClient

_LOGGER = logging.getLogger(__name__)


def _to_bool(bool_value: bool | str) -> bool:
    """Convert any value to bool."""
    if bool_value is not None:
        if isinstance(bool_value, bool):
            return bool_value
        if isinstance(bool_value, str):
            return bool_value.lower() == "true"
    return False


class DeviceModel:
    """Model of a device."""

    def __init__(self, data: dict, rest_client: RestClient, api_version: int) -> None:
        """Constructor."""
        self.raw_data = data
        self._rest_client = rest_client
        self._api_version = api_version

    @property
    def fibaro_id(self) -> int:
        """Device id"""
        return self.raw_data.get("id")

    @property
    def name(self) -> str:
        """Device name"""
        return self.raw_data.get("name")

    @property
    def parent_fibaro_id(self) -> int:
        """Id of the parent device or 0 if there is no parent."""
        return int(self.raw_data.get("parentId", 0))

    @property
    def type(self) -> str | None:
        """Device type."""
        return self.raw_data.get("type")

    @property
    def base_type(self) -> str | None:
        """Device base type."""
        return self.raw_data.get("baseType")

    @property
    def room_id(self) -> int:
        """Room id of the device or 0 if no room is assigned."""
        return int(self.raw_data.get("roomID", 0))

    @property
    def properties(self) -> dict:
        """Get the properties."""
        return self.raw_data.get("properties", {})

    @property
    def actions(self) -> dict[str, int]:
        """Get the available actions."""
        return self.raw_data.get("actions", {})

    def has_interface(self, interface_name: str) -> bool:
        """Returns True if the device has the according interface defined.

        Interfaces are capabilities of the device.
        """
        return interface_name in self.raw_data.get("interfaces", [])

    @property
    def unit(self) -> str | None:
        """Returns the unit of the devices value attribute or None."""
        return self.properties.get("unit")

    @property
    def has_unit(self) -> bool:
        """Returns true if the device has a unit property."""
        return "unit" in self.properties

    @property
    def endpoint_id(self) -> int:
        """Returns the endpoint id or 0 if there is no endpoint id.

        Endpoints are numbered if one physical device exposes different endpoints.
        """
        return int(self.properties.get("endPointId", 0))

    @property
    def has_endpoint_id(self) -> bool:
        """Returns true if the device has a unit property."""
        return "endPointId" in self.properties

    @property
    def enabled(self) -> bool:
        """Returns the enabled flag of the device.

        If the device does not support that flag, True is returned.
        """
        return self.raw_data.get("enabled", True)

    @property
    def visible(self) -> bool:
        """Returns the visible state of the device."""
        return self.raw_data.get("visible", True)

    @property
    def is_plugin(self) -> bool:
        """Returns the device type.

        Returns:
        True for virtual devices and Quick Apps
        False for physical devices and controllers
        """
        return self.raw_data.get("isPlugin", True)

    @property
    def battery_level(self) -> int:
        """Returns the battery level of the device in percent."""
        percent = int(self.properties.get("batteryLevel", 0))
        return 0 if percent == 255 else percent

    @property
    def has_battery_level(self) -> bool:
        """Returns true if the device has a unit property."""
        return "batteryLevel" in self.properties

    @property
    def armed(self) -> bool:
        """Returns the armed state of the device if supported,
        otherwise False is returned.
        """
        return _to_bool(self.properties.get("armed", False))

    @property
    def has_armed(self) -> bool:
        """Returns true if the device has a unit property."""
        return "armed" in self.properties

    @property
    def dead(self) -> bool:
        """Returns the state if the device is reachable if supported,
        otherwise False is returned.
        """
        return _to_bool(self.properties.get("dead", False))

    @property
    def has_dead(self) -> bool:
        """Returns true if the device has a dead property."""
        return "dead" in self.properties

    @property
    def dead_reason(self) -> str | None:
        """Returns the dead reason or None if not supported."""
        return self.properties.get("deadReason")

    @property
    def has_dead_reason(self) -> bool:
        """Returns true if the device has a deadReason property."""
        return "deadReason" in self.properties

    @property
    def value(self) -> ValueModel:
        """Returns the value info."""
        return ValueModel(self.properties, "value")

    @property
    def value_2(self) -> ValueModel:
        """Returns the value info."""
        return ValueModel(self.properties, "value2")

    @property
    def state(self) -> ValueModel:
        """Returns the state info."""
        return ValueModel(self.properties, "state")

    @property
    def color(self) -> ColorModel:
        """Returns the color info."""
        return ColorModel(self.properties, "color")

    @property
    def last_color_set(self) -> ColorModel:
        """Returns the last set color info."""
        return ColorModel(self.properties, "lastColorSet")

    @property
    def brightness(self) -> int:
        """Returns the brightness of the device. If missing, 0 is returned."""
        return int(self.properties.get("brightness", 0))

    @property
    def has_brightness(self) -> bool:
        """Returns true if the device has a brightness property."""
        return "brightness" in self.properties

    @property
    def current_program(self) -> int:
        """Returns the current program of the device or None."""
        return int(self.properties.get("currentProgram", 0))

    @property
    def current_program_id(self) -> int:
        """Returns the current pogram id of the device or 0."""
        return int(self.properties.get("currentProgramID", 0))

    @property
    def mode(self) -> int:
        """Returns the mode or 0 if there is no mode."""
        return int(self.properties.get("mode", 0))

    @property
    def has_mode(self) -> bool:
        """Returns true if the device has a mode."""
        return "mode" in self.properties

    @property
    def supported_modes(self) -> list[int]:
        """Returns the supported modes, for example for fan or hvac devices."""
        if "supportedModes" in self.properties:
            modes = self.properties.get("supportedModes")
            if isinstance(modes, str) and modes != "":
                return [int(mode) for mode in modes.split(",")]
            if isinstance(modes, list):
                return [int(mode) for mode in modes]
        return []

    @property
    def has_supported_modes(self) -> bool:
        """Returns true if the device has a supported modes property."""
        return "supportedModes" in self.properties

    @property
    def operating_mode(self) -> int:
        """Returns the operating mode or 0 if there is no operating mode."""
        return int(self.properties.get("operatingMode", 0))

    @property
    def has_operating_mode(self) -> bool:
        """Returns true if the device has a operating mode."""
        return "operatingMode" in self.properties

    @property
    def supported_operating_modes(self) -> list[int]:
        """Returns the supported operating modes, for example for fan or hvac devices."""
        if "supportedOperatingModes" in self.properties:
            modes = self.properties.get("supportedOperatingModes")
            if isinstance(modes, str) and modes != "":
                return [int(mode) for mode in modes.split(",")]
            if isinstance(modes, list):
                return [int(mode) for mode in modes]
        return []

    @property
    def has_supported_operating_modes(self) -> bool:
        """Returns true if the device has a supported operating modes property."""
        return "supportedOperatingModes" in self.properties

    @property
    def thermostat_mode(self) -> str | None:
        """Returns the thermostat mode or None if there is no thermostat mode."""
        return self.properties.get("thermostatMode")

    @property
    def has_thermostat_mode(self) -> bool:
        """Returns true if the device has a thermostat mode."""
        return "thermostatMode" in self.properties

    @property
    def thermostat_operating_state(self) -> str | None:
        """Returns the thermostat operating state or None if there
        is no thermostat operating state.
        """
        return self.properties.get("thermostatOperatingState")

    @property
    def has_thermostat_operating_state(self) -> bool:
        """Returns true if the device has a thermostat operating state."""
        return "thermostatOperatingState" in self.properties

    @property
    def supported_thermostat_modes(self) -> list[str]:
        """Returns the supported thermostat modes, for example for hvac devices."""
        if "supportedThermostatModes" in self.properties:
            return self.properties.get("supportedThermostatModes")
        return []

    @property
    def has_supported_thermostat_modes(self) -> bool:
        """Returns true if the device has a supported thermostat modes property."""
        return "supportedThermostatModes" in self.properties

    @property
    def heating_thermostat_setpoint(self) -> float:
        """Returns the heating thermostat setpoint or 0 if there is no value."""
        return float(self.properties.get("heatingThermostatSetpoint", 0.0))

    @property
    def has_heating_thermostat_setpoint(self) -> bool:
        """Returns true if the device has a heating thermostat setpoint property."""
        return "heatingThermostatSetpoint" in self.properties

    @property
    def heating_thermostat_setpoint_future(self) -> float:
        """Returns the heating thermostat setpoint future or 0 if there is no value."""
        return float(self.properties.get("heatingThermostatSetpointFuture", 0.0))

    @property
    def has_heating_thermostat_setpoint_future(self) -> bool:
        """Returns true if the device has a heating thermostat setpoint future property."""
        return "heatingThermostatSetpointFuture" in self.properties

    @property
    def target_level(self) -> float:
        """Returns the target level or 0 if there is no value."""
        return float(self.properties.get("targetLevel", 0.0))

    @property
    def has_central_scene_event(self) -> bool:
        """Returns true if the device can issue central scene events."""
        return "centralSceneSupport" in self.properties

    @property
    def central_scene_event(self) -> list[SceneEvent]:
        """Returns list of potential scene events."""
        central_scene_support = []
        value = self.properties.get("centralSceneSupport")
        if isinstance(value, list):
            central_scene_support = value
        if isinstance(value, str):
            central_scene_support = json.loads(value)

        result = []
        for central_scene in central_scene_support:
            key_id = int(central_scene.get("keyId"))
            key_attributes = central_scene.get("keyAttributes")
            result.append(SceneEvent(key_id, key_attributes))
        return result

    def execute_action(self, action: str, arguments: list[Any] | None = None) -> Any:
        """Execute a device action.

        Params:
        action: name of the action to call
        arguments: list of arguments needed for the action
        """
        if action not in self.actions:
            _LOGGER.warning(
                "The device %s has no action %s. Possible actions are %s",
                self.fibaro_id,
                action,
                self.actions,
            )

        url = f"devices/{self.fibaro_id}/action/{action}"

        args_prepared = {"args": arguments} if arguments else {}
        _LOGGER.debug(
            "Execute %s for device %s with args %s.",
            action,
            self.fibaro_id,
            args_prepared,
        )
        return self._rest_client.post(url, json=args_prepared)

    @staticmethod
    def read_devices(rest_client: RestClient, api_version: int) -> list[DeviceModel]:
        """Returns a list of devices."""
        raw_data: list[dict] = rest_client.get("devices")

        devices: list[dict] = []
        for device in raw_data:
            if device.get("type") in IGNORE_DEVICE:
                _LOGGER.debug("Ignore device: %s", device.get("id"))
            elif "id" not in device or "name" not in device:
                _LOGGER.debug(
                    "Ignore device because it does not contain id or name")
            else:
                devices.append(device)
        return [DeviceModel(data, rest_client, api_version) for data in devices]


class ValueModel:
    """Model to read out the value in several ways."""

    def __init__(self, properties: dict, property_name: str) -> None:
        """Constructor."""
        self._properties = properties
        self._property_name = property_name

    @property
    def has_value(self) -> bool:
        """Returns true if the device has a value property."""
        return self._property_name in self._properties

    @property
    def is_bool_value(self) -> bool:
        """Returns True if the device value property is a bool,
        either as string or real bool type.
        """
        if self.has_value:
            value = self._properties.get(self._property_name)
            if isinstance(value, bool):
                return True
            if isinstance(value, str):
                return value.lower() in ("true", "false")

        return False

    def str_value(self, default: str | None = None) -> str:
        """Returns the value converted to str or default if
        the object has no value.

        Raises:
        If no default is set, a TypeError is raised if no value exists.
        """
        if self.has_value:
            return str(self._properties.get(self._property_name, default))

        if default is None:
            raise TypeError("No value attribute available")
        return default

    def int_value(self, default: int | None = None) -> int:
        """Returns the value converted to int or default if
        conversion failes or the object has no value.

        Raises:
        If no default is set, a TypeError is raised for invalid values.
        """
        try:
            return int(float(self._properties.get(self._property_name)))
        except TypeError as ex:
            if default is None:
                raise ex
            return default

    def float_value(self, default: float | None = None) -> float:
        """Returns the value converted to float or default if
        conversion failes or the object has no value.

        Raises:
        If no default is set, a TypeError is raised for invalid values.
        """
        try:
            return float(self._properties.get(self._property_name))
        except TypeError as ex:
            if default is None:
                raise ex
            return default

    def bool_value(self, default: bool | None = None) -> bool:
        """Returns the value converted to bool or default if
        conversion failes or the object has no value.

        Raises:
        If no default is set, a TypeError is raised for invalid values.
        """
        try:
            value = self._properties.get(self._property_name)
            if isinstance(value, bool):
                return value
            if isinstance(value, str):
                if self.is_bool_value:
                    return value.lower() == "true"
                return self.float_value(0) != 0
            if isinstance(value, (int, float)):
                return value != 0
            raise TypeError(f"Value cannot be converted to bool {value}")
        except TypeError as ex:
            if default is None:
                raise ex
            return default

    def dict_value(self, default: dict | None = None) -> dict:
        """Returns the value converted to a dict or default if
        conversion failes or the object has no value.

        Raises:
        If no default is set, an error is raised for invalid values.
        """
        try:
            value = self._properties.get(self._property_name)
            if isinstance(value, dict):
                return value
            if isinstance(value, str):
                return json.loads(value)
            raise TypeError(f"Value cannot be converted to dict {value}")
        except TypeError as ex:
            if default is None:
                raise ex
            return default


class ColorModel:
    """Model to read out the color."""

    def __init__(self, properties: dict, property_name: str) -> None:
        """Constructor."""
        self._properties = properties
        self._property_name = property_name

    @property
    def has_color(self) -> bool:
        """Returns true if the device has a value property."""
        if self._property_name in self._properties:
            try:
                self.rgbw_color
                return True
            except (TypeError, ValueError):
                return False
        return False

    @property
    def rgbw_color(self) -> tuple[int, int, int, int]:
        """Returns the color as RGBW value.
        For RGB devices the white value is reported as 0.

        Raises:
        TypeError is raised for invalid values.
        """
        color = self._properties.get(self._property_name)
        if color is None:
            raise TypeError("Color is None.")
        rgbw = tuple(int(i) for i in color.split(","))
        if len(rgbw) != 4:
            raise TypeError(f"Color does not have 4 parts: {color}")
        return rgbw


class SceneEvent:
    """Model to read out the scene events."""

    def __init__(self, key_id: int, key_attributes: list[str]) -> None:
        """Constructor."""
        self._key_id = key_id
        self._key_attributes = key_attributes

    @property
    def key_id(self) -> int:
        """Returns the key id."""
        return self._key_id

    @property
    def key_event_types(self) -> list[str]:
        """Returns the possible key event types."""
        return self._key_attributes
