"""
client module

contains:

    Client: class

"""

__all__ = ("Client",)

import base64
import datetime
import hashlib
from dataclasses import dataclass

from ..utils import Base, BakalibError, _setup_logger


def _is_logged_in(invert: bool = False):
    """Checks if arg[0] (Client) is logged in.
    
    :param invert: Inverts the check, defaults to False
    :type invert: bool, optional
    :raises BakalibError: If Client is not logged in
    :raises BakalibError: If Client is already logged in
    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            if args[0].logged_in ^ invert:
                return func(*args, **kwargs)
            else:
                if invert:
                    raise BakalibError("Client is already logged in.")
                raise BakalibError("Client is not logged in.")

        return wrapper

    return decorator


class Client(Base):
    """This is a class representing a user on a school server. It needs to be logged in (:meth:`.Client.login`) before using with other components. 
        
    :param username: Name of the user
    :type username: str
    :param url: URL address of the school server
    :type url: str
    """

    username: str
    url: str

    perm_token: str
    token: str

    logged_in: bool

    @dataclass(frozen=True)
    class Info:
        version: str
        name: str
        type_abbr: str
        type: str
        school: str
        school_type: str
        class_: str
        year: str
        modules: str
        newmarkdays: str

    def __init__(self, username: str, url: str):
        """Constructor method
        """
        self.username = username
        self.url = url

        self.perm_token = None
        self.token = None

        self.logged_in = False

        self.logger = _setup_logger(f"[CLIENT_{self.username}]")
        self.logger.debug(f"INITIALIZED")

    def __str__(self):
        return f"Client(username={self.username}, url={self.url})"

    def __repr__(self):
        return {"username": self.username, "url": self.url}

    @_is_logged_in(invert=True)
    def login(
        self, password: str = None, perm_token: str = None, check_valid: bool = True
    ):
        """Logs the client in
        
        :param password: Password, defaults to None
        :type password: str, optional
        :param perm_token: Permanent token generated from password, defaults to None
        :type perm_token: str, optional
        :param check_valid: Checks if the session is valid after logging in, defaults to True
        :type check_valid: bool, optional
        :raises BakalibError: If incorrect arguments are provided
        :raises BakalibError: If generated login token is invalid (wrong password/perm_token)
        """
        if perm_token:
            self.perm_token = perm_token
            token = self._token(self.perm_token)
        elif password:
            self.perm_token = self._permanent_token(password)
            token = self._token(self.perm_token)
        else:
            raise BakalibError("Incorrect arguments")

        if check_valid:
            if self._is_token_valid(token):
                self.token = token
            else:
                raise BakalibError("Token is invalid: Invalid password/perm_token")
        else:
            self.token = token

        self.logged_in = True
        self.logger.info(f"LOGGED IN SUCCESSFULLY")

    def _permanent_token(self, password: str) -> str:
        """Generates a permanent token which can be used for login
        
        :param password: Password
        :type password: str
        :raises BakalibError: If the username is not found on the server
        :return: Permanent token
        :rtype: str
        """
        self.logger.debug("GENERATING PERM_TOKEN")
        try:
            resp = self.request(gethx=self.username)
        except BakalibError as e:
            self.logger.warning("PERM_TOKEN IS INVALID")
            raise BakalibError("Invalid username")

        salt = resp.get("salt")
        ikod = resp.get("ikod")
        typ = resp.get("typ")
        salted_password = f"{salt}{ikod}{typ}{password}".encode("utf-8")
        hashed_password = base64.b64encode(hashlib.sha512(salted_password).digest())
        perm_token = (
            f"*login*{self.username}*pwd*{hashed_password.decode('utf8')}*sgn*ANDR"
        )
        self.logger.debug("PERM_TOKEN GENERATED")
        return perm_token

    def _token(self, perm_token: str) -> str:
        """Generates an access token using current time
        
        :param perm_token: Permanent token generated by :meth:`.Client._permanent_token`
        :type perm_token: str
        :return: Access token
        :rtype: str
        """
        self.logger.debug("GENERATING ACCESS TOKEN")
        today = datetime.date.today()
        datecode = "{:04}{:02}{:02}".format(today.year, today.month, today.day)
        token_hash = hashlib.sha512((perm_token + datecode).encode("utf-8")).digest()
        token = base64.urlsafe_b64encode(token_hash).decode("utf-8")
        self.logger.debug("ACCESS TOKEN GENERATED")
        return token

    def _is_token_valid(self, token: str) -> bool:
        """Checks if generated token from :meth:`.Client._token` is valid
        
        :param token: Access token
        :type token: str
        :return: True of False, depends on the result
        :rtype: bool
        """
        try:
            self.request(hx=token, pm="login")
            self.logger.info(f"TOKEN IS VALID")
            return True
        except BakalibError:
            self.logger.warning(f"TOKEN IS INVALID")
            return False

    @_is_logged_in()
    def info(self) -> "Client.Info":
        """Fetches information about the current user
        
        :return: Dataclass containing user information
        :rtype: Client.Info
        """

        response = self.request(hx=self.token, pm="login")
        result = self.Info(
            *[
                response.get(element).get("newmarkdays")
                if element == "params"
                else response.get(element)
                for element in response
                if not element == "result"
            ]
        )
        return result
