from __future__ import annotations

import base64
import json
import logging
import random

from aiohttp import ClientSession
from dataclasses import dataclass
from datetime import datetime
from typing import Any, List, Optional
from urllib.parse import quote
from yarl import URL

from .const import (
    HARDWARE_VERSION_OID,
    MAC_ADDRESS_OID,
    SERIAL_NUMBER_OID,
    SOFTWARE_VERSION_OID,
    TOKEN_EXPIRATION,
    USERNAME,
)
from .device import Device
from .exception import ConnectBoxError, InvalidCredentialError
from .mib_mapper import format_mac, to_devices

_LOGGER = logging.getLogger(__name__)


class ConnectBox:
    def __init__(self, websession: ClientSession, hostname: str, password: str):
        self._websession = websession
        self._hostname = hostname
        self._password = password
        self._nonce = str(random.randrange(10000, 100000))
        self._credential: Optional[Credential] = None

    async def async_login(self) -> str:
        arg_string = f"{quote(USERNAME)}:{quote(self._password)}"
        arg = base64.b64encode(arg_string.encode("utf-8")).decode("ascii")

        async with self._websession.get(f"{self._hostname}/login?arg={arg}&_n={self._nonce}") as response:
            response.raise_for_status()

            token = await response.text()

            _LOGGER.debug("Received token: %s", token)

            self._credential = Credential(token)
            self._credential.validate()

            return token

    async def async_logout(self) -> None:
        credential = await self._async_get_credential()

        params = {"_n": self._nonce}
        cookies = {"credential": credential.token}

        async with self._websession.get(f"{self._hostname}/logout", params=params, cookies=cookies) as response:
            if response.status != 500:
                response.raise_for_status()
            self._credential = None

    async def async_get_connected_devices(self, retry_on_unauthorized=True) -> List[Device]:
        """Get all connected devices.

        A device is returned for every IP address. When a device has both a IPv4 and a IPv6 address it will appear
        twice in the returned list.
        """
        credential = await self._async_get_credential()

        params = {"_n": self._nonce}
        cookies = {"credential": credential.token}
        async with self._websession.get(f"{self._hostname}/getConnDevices", params=params, cookies=cookies) as response:
            if retry_on_unauthorized is True and response.status == 401:
                self._credential = None
                return await self.async_get_connected_devices(False)

            response.raise_for_status()

            response_text = await response.text()

            _LOGGER.debug("getConnDevices response: %s", response_text)

            return to_devices(response_text)

    async def async_get_router_information(self) -> RouterInformation:
        oids = [
            MAC_ADDRESS_OID,
            HARDWARE_VERSION_OID,
            SOFTWARE_VERSION_OID,
            SERIAL_NUMBER_OID,
        ]

        snmp_get_result = await self._async_snmp_get(oids)

        router_information = RouterInformation(
            format_mac(snmp_get_result[MAC_ADDRESS_OID]),
            snmp_get_result[HARDWARE_VERSION_OID],
            snmp_get_result[SOFTWARE_VERSION_OID],
            snmp_get_result[SERIAL_NUMBER_OID],
        )
        return router_information

    async def _async_get_credential(self) -> Credential:
        if self._credential is None or self._credential.is_expired():
            await self.async_login()

        return self._credential  # type: ignore

    async def _async_snmp_get(self, oids: List[str]) -> Any:
        credential = await self._async_get_credential()

        # Manually create url because otherwise the semicolons are url encoded.
        oids_joined = ";".join(oids)
        url = f"{self._hostname}/snmpGet?oids={oids_joined}&_n={self._nonce}"
        cookies = {"credential": credential.token}

        _LOGGER.debug("Get SNMP query %s results for router %s", oids, self._hostname)
        async with self._websession.get(URL(url), cookies=cookies) as response:
            response.raise_for_status()

            data = await response.text()

            # Response starts with "Error in OID formatting!" when an invalid OID is requested.
            if data.startswith("Error"):
                raise ConnectBoxError(data)

            _LOGGER.debug("snmpGet response: %s", data)

            return json.loads(data)


class Credential:
    def __init__(self, token: str):
        self._token = token
        self._created_at = datetime.now()

    def is_expired(self) -> bool:
        expires = self._created_at + TOKEN_EXPIRATION
        return datetime.now() > expires

    def validate(self) -> None:
        try:
            self._token_as_json()
        except Exception as exc:
            raise InvalidCredentialError from exc

    def is_multi_login(self) -> bool:
        """Return if the user is already logged in."""

        is_logged_in_value = self._is_logged_in()

        return is_logged_in_value > 0

    @property
    def token(self) -> str:
        return self._token

    def _is_logged_in(self) -> int:
        """Return if the user is already logged in.

        Reverse engineered from a javascript library of the routers web application. The response of this method doesn't
        indicate if a consecutive call will fail or succeed. Method is kept for reference.
        """
        token_json = self._token_as_json()

        if "muti" in token_json:
            muti = token_json["muti"]
            con_type = token_json["conType"]
            gw_wan = token_json["gwWan"]

            if gw_wan == "f" and con_type == "LAN" and muti == "GW_WAN":
                return 2  # Remote user already login.
            elif gw_wan == "t" and muti == "LAN":
                return 1  # Local user already login.
            if gw_wan == "f" and con_type == "LAN" and muti == "LAN":
                return 3  # Other local user already login.
            elif gw_wan == "t" and muti == "GW_WAN":
                return 4  # Other remote user already login.

        return 0

    def _token_as_json(self) -> Any:
        decoded_token = base64.b64decode(self._token)
        return json.loads(decoded_token)


@dataclass
class RouterInformation:
    mac_address: str
    hardware_version: str
    software_version: str
    serial_number: str
