import dataclasses
import typing
import urllib.parse
from datetime import datetime, timedelta
from enum import Enum, IntEnum

import retry
import requests

from zeversolar.exceptions import ZeverSolarTimeout, ZeverSolarHTTPError, ZeverSolarHTTPNotFound, ZeverSolarInvalidData, \
    ZeverSolarError

kWh = typing.NewType("kWh", float)  # pragma: no mutate
Watt = typing.NewType("Watt", int)  # pragma: no mutate


class PowerMode(IntEnum):
    ON = 0
    OFF = 1


class Values(IntEnum):
    WIFI_ENABLED = 0           # bool (0|1)
    # ? = 1                    # int
    SERIAL_OR_REGISTRY_ID = 2  # string
    REGISTRY_KEY = 3           # string
    HARDWARE_VERSION = 4       # string
    SOFTWARE_VERSION = 5       # string
    REPORTED_TIME = 6          # HH:MM
    REPORTED_DATE = 7          # DD/MM/YYYY
    COMMUNICATION_STATUS = 8   # int|OK|error
    NUM_INVERTERS = 9          # int (0-4)
    INVERTERS = 10             # start of inverter data


class StatusEnum(Enum):
    OK = "OK"
    ERROR = "ERROR"


@dataclasses.dataclass
class ZeverSolarData:
    wifi_enabled: bool
    serial_or_registry_id: str
    registry_key: str
    hardware_version: str
    software_version: str
    reported_datetime: datetime
    communication_status: StatusEnum
    num_inverters: int
    serial_number: str
    pac: Watt
    energy_today: kWh
    status: StatusEnum
    meter_status: StatusEnum


class ZeverSolarParser:
    def __init__(self, zeversolar_response: str):
        self.zeversolar_response = zeversolar_response

    def parse(self) -> ZeverSolarData:
        response_parts = self.zeversolar_response.split()

        if len(response_parts) <= Values.NUM_INVERTERS.value:
            raise ZeverSolarInvalidData()

        wifi_enabled = response_parts[Values.WIFI_ENABLED] == "1"
        serial_or_registry_id = response_parts[Values.SERIAL_OR_REGISTRY_ID]
        registry_key = response_parts[Values.REGISTRY_KEY]
        hardware_version = response_parts[Values.HARDWARE_VERSION]
        software_version = response_parts[Values.SOFTWARE_VERSION]

        reported_time = response_parts[Values.REPORTED_TIME]
        reported_date = response_parts[Values.REPORTED_DATE]
        try:
            reported_datetime = datetime.strptime(f"{reported_date} {reported_time}", "%d/%m/%Y %H:%M")
        except ValueError as exception:
            raise ZeverSolarInvalidData() from exception

        communication_status_value = response_parts[Values.COMMUNICATION_STATUS]
        try:
            communication_status = StatusEnum(communication_status_value.upper())
        except ValueError as exception:
            if not communication_status_value.isnumeric():
                raise ZeverSolarInvalidData from exception
            communication_status = StatusEnum.OK if communication_status_value == "0" else StatusEnum.ERROR

        try:
            num_inverters = int(response_parts[Values.NUM_INVERTERS])
        except ValueError as exception:
            raise ZeverSolarInvalidData() from exception

        if num_inverters < 1:
            raise ZeverSolarInvalidData()

        index = Values.INVERTERS.value

        serial_number = response_parts[index]
        index += 1

        try:
            pac = Watt(int(response_parts[index]))
        except ValueError:
            # ? = response_parts[index]
            index += 1
            try:
                pac = Watt(int(response_parts[index]))
            except ValueError as exception:
                raise ZeverSolarInvalidData() from exception
        index += 1

        try:
            energy_today = kWh(self._fix_leading_zero(response_parts[index]))
        except ValueError as exception:
            raise ZeverSolarInvalidData() from exception
        index += 1

        try:
            status = StatusEnum(response_parts[index].upper())
        except ValueError as exception:
            raise ZeverSolarInvalidData() from exception
        index += 1

        try:
            meter_status = StatusEnum(response_parts[index].upper())
        except ValueError as exception:
            raise ZeverSolarInvalidData() from exception

        return ZeverSolarData(
            wifi_enabled=wifi_enabled,
            serial_or_registry_id=serial_or_registry_id,
            registry_key=registry_key,
            hardware_version=hardware_version,
            software_version=software_version,
            reported_datetime=reported_datetime,
            communication_status=communication_status,
            num_inverters=num_inverters,
            serial_number=serial_number,
            pac=pac,
            energy_today=energy_today,
            status=status,
            meter_status=meter_status,
        )

    @staticmethod
    def _fix_leading_zero(string_value: str) -> float:
        split_values = string_value.split(".")
        if len(decimals := split_values[1]) == 1:
            string_value = f"{split_values[0]}.0{decimals}"
        return float(string_value)


class ZeverSolarClient:
    def __init__(self, host: str):
        if "http" not in host:
            # noinspection HttpUrlsUsage
            host = f"http://{host}"
        self.host = urllib.parse.urlparse(url=host).netloc.strip("/")
        self._timeout = timedelta(seconds=10).total_seconds()
        self._serial_number: typing.Optional[str] = None

    @retry.retry(exceptions=(ZeverSolarTimeout, ZeverSolarInvalidData), tries=3)  # pragma: no mutate
    def get_data(self) -> ZeverSolarData:
        try:
            response = requests.get(url=f"http://{self.host}/home.cgi", timeout=self._timeout)
        except requests.exceptions.Timeout as exception:
            raise ZeverSolarTimeout() from exception
        except Exception as exception:
            raise ZeverSolarError() from exception

        try:
            response.raise_for_status()
        except requests.exceptions.HTTPError as exception:
            if response.status_code == 404:
                raise ZeverSolarHTTPNotFound() from exception
            raise ZeverSolarHTTPError() from exception

        return ZeverSolarParser(zeversolar_response=response.text).parse()

    def power_on(self) -> PowerMode:
        return self.ctrl_power(mode=PowerMode.ON)

    def power_off(self) -> PowerMode:
        return self.ctrl_power(mode=PowerMode.OFF)

    @retry.retry(exceptions=ZeverSolarTimeout, tries=3)  # pragma: no mutate
    def ctrl_power(self, mode: PowerMode) -> PowerMode:
        if self._serial_number is None:
            self._serial_number = self.get_data().serial_number

        try:
            response = requests.post(
                url=f"http://{self.host}/inv_ctrl.cgi",
                data={'sn': self._serial_number, 'mode': mode.value},
                timeout=self._timeout,
            )
        except requests.exceptions.Timeout as exception:
            raise ZeverSolarTimeout() from exception
        except Exception as exception:
            raise ZeverSolarError() from exception

        try:
            response.raise_for_status()
        except requests.exceptions.HTTPError as exception:
            if response.status_code == 404:
                raise ZeverSolarHTTPNotFound() from exception
            raise ZeverSolarHTTPError() from exception

        return mode
