"""Python Library for communicating with PTDevices."""

from enum import StrEnum
from http import HTTPStatus
import logging
from typing import Any, TypedDict, Required

import orjson
import asyncio
from string import ascii_letters

from aioptdevices.errors import (
    PTDevicesForbiddenError,
    PTDevicesRequestError,
    PTDevicesUnauthorizedError,
)

from .configuration import Configuration

LOGGER = logging.getLogger(__name__)

type PTDevicesResponseData = dict[str, dict[str, Any]]


class PTDevicesResponse(TypedDict, total=False):
    """Typed Response from PTDevices."""

    body: Required[dict[str, dict[str, Any]]]
    code: Required[int]


class PTDevicesBatteryState(StrEnum):
    """Store keys for hte different battery_status states."""

    UNKNOWN = "unknown"
    GOOD = "good"
    LOW = "low"


_battery_value_translations: dict[int, str] = {
    1: PTDevicesBatteryState.GOOD,
    2: PTDevicesBatteryState.LOW,
}


class PTDevicesStatusStates(StrEnum):
    """Store keys for the different device_status states."""

    UNKNOWN = "unknown"
    WORKING = "working"
    NOT_CONNECTED_YET = "not_connected_yet"
    NOT_CONNECTED = "not_connected"
    TRANSMITTER_NOT_REPORTING = "transmitter_not_reporting"
    PRESS_TRANSMITTER_CONNECT_BUTTON = "press_transmitter_connect_button"
    POWER_INTERNET_OUT_OR_RECEIVER_NOT_WORKING = (
        "power_internet_out_or_receiver_not_working"
    )


_status_value_translations: dict[int, str] = {
    0: PTDevicesStatusStates.NOT_CONNECTED_YET,
    1: PTDevicesStatusStates.NOT_CONNECTED_YET,
    2: PTDevicesStatusStates.WORKING,
    3: PTDevicesStatusStates.NOT_CONNECTED,
    4: PTDevicesStatusStates.TRANSMITTER_NOT_REPORTING,
    5: PTDevicesStatusStates.POWER_INTERNET_OUT_OR_RECEIVER_NOT_WORKING,
    6: PTDevicesStatusStates.PRESS_TRANSMITTER_CONNECT_BUTTON,
}

_convert_to_number_keys: dict[str, type] = {
    "version": int,
    "wifi_signal": int,
    "tx_signal": float,
    "percent_level": float,
    "battery_voltage": float,
    "volume_level": float,
    "inch_level": float,
    "probe_temperature": float,
}


def _format_data(
    input: list[dict[str, Any]],
) -> dict[str, dict[str, Any]]:
    """Format the returned data into a more helpful form. This includes putting id's as keys and flattening the dict."""
    output: dict[str, dict[str, Any]] = {}

    # Add id's as keys
    devices: dict[str, dict[str, Any]] = {
        device.get("device_id", ""): device for device in input
    }

    # Flatten the dict

    # Recurse through key value pairs and convert from nested to flat
    def recurse(sub_dict: dict[str, dict[str, Any]]) -> dict[str, Any]:
        results: dict[str, Any] = {}

        for key, value in sub_dict.items():
            if isinstance(value, dict):
                results = {**results, **recurse(value)}
            else:
                results[key] = value

        return results

    for device_id, device in devices.items():
        output[device_id] = recurse(device)

    # At this point the data has been transformed into a flat dict
    # {"id":{...info...},"id2":{...info...}}

    for device_id, device in output.items():
        # Translate the device status number
        if device["status_number"] in _status_value_translations.keys():
            output[device_id]["status"] = str(
                _status_value_translations[device["status_number"]]
            )
        else:
            output[device_id]["status"] = str(
                PTDevicesStatusStates.UNKNOWN
            )  # TODO tests dont cover this

        # Translate the battery status number
        if device.get("battery_status_number") is not None:
            if device["battery_status_number"] in _battery_value_translations.keys():
                output[device_id]["battery_status"] = str(
                    _battery_value_translations[device["battery_status_number"]]
                )
            else:
                output[device_id]["battery_status"] = str(
                    PTDevicesBatteryState.UNKNOWN
                )  # TODO tests dont cover this

        # Change all measurements to float | int | str
        for key in _convert_to_number_keys.keys():
            if device.get(key) is not None and isinstance(device.get(key), str):
                output[device_id][key] = _convert_to_number_keys[key](
                    device.get(key, "").strip(ascii_letters + "%"),
                )

        # Change the inch_level to depth_level and convert to metric (meters)
        if device.get("inch_level") is not None:
            output[device_id]["depth_level"] = round(
                0.0254 * float(device.get("inch_level", 0.0)), 6
            )
            output[device_id].pop("inch_level")

        if device.get("volume_level") is not None:
            if device.get("units", "") == "US Imperial":
                output[device_id]["volume_level"] = round(
                    3.785411784 * float(device.get("volume_level", 0.0)), 6
                )

            if device.get("units", "") == "British Imperial":
                output[device_id]["volume_level"] = round(
                    4.54609 * float(device.get("volume_level", 0.0)), 6
                )

        if device.get("probe_temperature") is not None:
            if device.get("temperature_units", "") == "F":
                output[device_id]["probe_temperature"] = round(
                    (float(device.get("probe_temperature", 0.0)) - 32) * 0.5555555556, 3
                )

    return output


class Interface:
    """Interface for PTDevices."""

    def __init__(self, config: Configuration) -> None:
        """Initilize object variables."""
        self.config = config

    async def get_data(self) -> PTDevicesResponse:
        """Fetch device data from PTDevices server and format it."""
        # Request url: https://api.ptdevices.com/token/v1/device/{deviceId}?api_token={given_token}
        # Where
        #   {deviceId} is the numeric internal device id,
        #       found in the url https://www.ptdevices.com/device/level/{deviceId}
        #   {given_token} is the access token you were given
        #
        # Request url: https://api.ptdevices.com/token/v1/devices?api_token={given_token}
        # Where
        #   {given_token} is the access token you were given

        # Construct the URL differently for multi device and single device requests
        if self.config.device_id == "*":
            url = f"{self.config.url}/devices?api_token={self.config.auth_token}"
            LOGGER.debug(
                "Sending request to %s for data from all devices",
                self.config.url,
            )
        else:
            url = f"{self.config.url}/device/{self.config.device_id}?api_token={self.config.auth_token}"
            LOGGER.debug(
                "Sending request to %s for data from device %s",
                self.config.url,
                self.config.device_id,
            )

        try:
            async with asyncio.timeout(10):
                async with self.config.session.request(
                    "get",
                    url,
                    allow_redirects=False,
                ) as results:
                    LOGGER.debug(
                        "%s Received from %s",
                        results.status,
                        self.config.url,
                    )

                    # Check return code
                    if results.status == HTTPStatus.UNAUTHORIZED:  # 401
                        raise PTDevicesUnauthorizedError(
                            f"Request to {url.split('?api_token')[0]} failed, the token provided is not valid"
                        )
                    if results.status == HTTPStatus.FOUND:  # 302
                        # Back end currently returns a 302 when request is not authorized
                        raise PTDevicesUnauthorizedError(
                            f"Request to {url.split('?api_token')[0]} failed, the token provided is not valid (302)"
                        )

                    if results.status == HTTPStatus.FORBIDDEN:  # 403
                        raise PTDevicesForbiddenError(
                            f"Request to {url.split('?api_token')[0]} failed, token invalid for device {self.config.device_id}"
                        )

                    if results.status != HTTPStatus.OK:  # anything but 200
                        raise PTDevicesRequestError(
                            f"Request to {url.split('?api_token')[0]} failed, got unexpected response from server ({results.status})"
                        )

                    # Check content type
                    if (
                        results.content_type != "application/json"
                        or results.content_length == 0
                    ):
                        raise PTDevicesRequestError(
                            f"Failed to get device data, returned content is invalid. Type: {results.content_type}, content Length: {results.content_length}, content: {results.content}"
                        )

                    raw_json = await results.read()

                    body = orjson.loads(raw_json)
                    formatted_data: dict[str, dict[str, Any]] = _format_data(
                        body["data"] if type(body["data"]) is list else [body["data"]]
                    )

                    # Store the new data to the response and return
                    return PTDevicesResponse(code=results.status, body=formatted_data)

        except TimeoutError:
            # If the request timed out, throw a request error
            raise PTDevicesRequestError(
                f"Request to {url.split('?api_token')[0]} failed, request timed out"
            )
