"""Entry point. Authorizes on Aternos
and allows to manage your account"""

import os
import re
import hashlib

from typing import List, Optional

import lxml.html

from .atserver import AternosServer
from .atconnect import AternosConnect
from .aterrors import CredentialsError


class Client:

    """Aternos API Client class object of which contains user's auth data

    :param atconn: :class:`python_aternos.atconnect.AternosConnect`
    instance with initialized Aternos session
    :type atconn: python_aternos.atconnect.AternosConnect
    """

    def __init__(
            self,
            atconn: AternosConnect,
            servers: Optional[List[str]] = None) -> None:

        self.atconn = atconn
        self.parsed = False
        self.servers: List[AternosServer] = []

        if servers:
            self.refresh_servers(servers)

    @classmethod
    def from_hashed(cls, username: str, md5: str):

        """Log in to Aternos with a username and a hashed password

        :param username: Your username
        :type username: str
        :param md5: Your password hashed with MD5
        :type md5: str
        :raises CredentialsError: If the API
        doesn't return a valid session cookie
        :return: Client instance
        :rtype: python_aternos.Client
        """

        atconn = AternosConnect()
        atconn.parse_token()
        atconn.generate_sec()

        credentials = {
            'user': username,
            'password': md5
        }

        loginreq = atconn.request_cloudflare(
            'https://aternos.org/panel/ajax/account/login.php',
            'POST', data=credentials, sendtoken=True
        )

        if 'ATERNOS_SESSION' not in loginreq.cookies:
            raise CredentialsError(
                'Check your username and password'
            )

        return cls(atconn)

    @classmethod
    def from_credentials(cls, username: str, password: str):

        """Log in to Aternos with a username and a plain password

        :param username: Your username
        :type username: str
        :param password: Your password without any encryption
        :type password: str
        :return: Client instance
        :rtype: python_aternos.Client
        """

        md5 = Client.md5encode(password)
        return cls.from_hashed(username, md5)

    @classmethod
    def from_session(
            cls,
            session: str,
            servers: Optional[List[str]] = None):

        """Log in to Aternos using a session cookie value

        :param session: Value of ATERNOS_SESSION cookie
        :type session: str
        :return: Client instance
        :rtype: python_aternos.Client
        """

        atconn = AternosConnect()
        atconn.session.cookies['ATERNOS_SESSION'] = session
        atconn.parse_token()
        atconn.generate_sec()

        return cls(atconn, servers)

    @classmethod
    def restore_session(cls, file: str = '~/.aternos'):

        """Log in to Aternos using a saved ATERNOS_SESSION cookie

        :param file: File where a session cookie
        was saved, deafults to `~/.aternos`
        :type file: str, optional
        :return: Client instance
        :rtype: python_aternos.Client
        """

        file = os.path.expanduser(file)
        with open(file, 'rt', encoding='utf-8') as f:
            saved = f.read().replace('\r\n', '\n').split('\n')

        session = saved[0].strip()

        if len(saved) > 1:
            return cls.from_session(
                session=session,
                servers=saved[1:]
            )

        return cls.from_session(session)

    @staticmethod
    def md5encode(passwd: str) -> str:

        """Encodes the given string with MD5

        :param passwd: String to encode
        :type passwd: str
        :return: Hexdigest hash of the string in lowercase
        :rtype: str
        """

        encoded = hashlib.md5(passwd.encode('utf-8'))
        return encoded.hexdigest().lower()

    def save_session(
            self,
            file: str = '~/.aternos',
            incl_servers: bool = True) -> None:

        """Saves an ATERNOS_SESSION cookie to a file

        :param file: File where a session cookie
        must be saved, defaults to `~/.aternos`
        :type file: str, optional
        :param incl_servers: If the function
        should include the servers IDs to
        reduce API requests count (recommended),
        defaults to True
        :type incl_servers: bool, optional
        """

        file = os.path.expanduser(file)
        with open(file, 'wt', encoding='utf-8') as f:

            f.write(self.atconn.atsession + '\n')
            if not incl_servers:
                return

            for s in self.servers:
                f.write(s.servid + '\n')

    def list_servers(self, cache: bool = True) -> List[AternosServer]:

        """Parses a list of your servers from Aternos website

        :param cache: If the function should use
        cached servers list (recommended), defaults to True
        :type cache: bool, optional
        :return: List of :class:`python_aternos.atserver.AternosServer` objects
        :rtype: list
        """

        if cache and self.parsed:
            return self.servers

        serverspage = self.atconn.request_cloudflare(
            'https://aternos.org/servers/', 'GET'
        )
        serverstree = lxml.html.fromstring(serverspage.content)

        servers = serverstree.xpath(
            '//div[@class="server-body"]/@data-id'
        )
        self.refresh_servers(servers)

        return self.servers

    def refresh_servers(self, ids: List[str]) -> None:

        """Replaces cached servers list creating
        :class:`AternosServer` objects by given IDs

        :param ids: Servers unique identifiers
        :type ids: List[str]
        """

        self.servers = []
        for s in ids:

            servid = s.strip()
            if servid == '':
                continue

            srv = AternosServer(servid, self.atconn)
            self.servers.append(srv)

        self.parsed = True

    def get_server(self, servid: str) -> AternosServer:

        """Creates a server object from the server ID.
        Use this instead of list_servers if you know the ID to save some time.

        :return: :class:`python_aternos.atserver.AternosServer` object
        :rtype: python_aternos.atserver.AternosServer
        """

        return AternosServer(servid, self.atconn)

    def logout(self) -> None:

        """Logouts from Aternos account"""

        self.atconn.request_cloudflare(
            'https://aternos.org/panel/ajax/account/logout.php',
            'GET', sendtoken=True
        )

    def change_username(self, value: str) -> None:

        """Changes a username in your Aternos account

        :param value: New username
        :type value: str
        """

        self.atconn.request_cloudflare(
            'https://aternos.org/panel/ajax/account/username.php',
            'POST', data={'username': value}, sendtoken=True
        )

    def change_email(self, value: str) -> None:

        """Changes an e-mail in your Aternos account

        :param value: New e-mail
        :type value: str
        :raises ValueError: If an invalid
        e-mail address was passed to the function
        """

        email = re.compile(
            r'^[A-Za-z0-9\-_+.]+@[A-Za-z0-9\-_+.]+\.[A-Za-z0-9\-]+$|^$'
        )
        if not email.match(value):
            raise ValueError('Invalid e-mail!')

        self.atconn.request_cloudflare(
            'https://aternos.org/panel/ajax/account/email.php',
            'POST', data={'email': value}, sendtoken=True
        )

    def change_password(self, old: str, new: str) -> None:

        """Changes a password in your Aternos account

        :param old: Old password
        :type old: str
        :param new: New password
        :type new: str
        """

        old = Client.md5encode(old)
        new = Client.md5encode(new)
        self.atconn.request_cloudflare(
            'https://aternos.org/panel/ajax/account/password.php',
            'POST', data={
                'oldpassword': old,
                'newpassword': new
            }, sendtoken=True
        )
