import base64
import logging
import os
import time
from http.cookies import SimpleCookie
from typing import Optional

from clarabot_crypto.auth.login import (
    create_account_login_request_params,
    create_account_login_private_keyring,
    create_account_login_keyring,
    create_account_login_session,
)
from clarabot_crypto.keyring.private import PRIVATE_KEYRING_CLASSES, AbstractPrivateKeyring
from clarabot_nano_common.common.auth.handler import AbstractServerAuthenticator, LoginFailed
from clarabot_nano_common.common.crypto.keyring import NanoSelfAccountKeyring

from clarabot_nano.common.auth.two_factor import TwoFactorError, gateway_to_control

logger = logging.getLogger(__name__)


class ServerAuthenticator(AbstractServerAuthenticator):
    async def login(
        self,
        email: str,
        password: str,
        timeout=10.0,
        server_session_key: Optional[bytes] = None,
        otp_salt: Optional[str] = None,
        tfa_code: Optional[str] = None,
    ) -> NanoSelfAccountKeyring:
        if not server_session_key:
            # NOTE: For some 2FA logins this must be provided by the client code to have it be consistent.
            server_session_key = os.urandom(16)
        t = time.perf_counter()
        # 1. acquire salt for PPF
        async with self.post_auto_csrf(
            f"https://{self._d}{self._p}r/login-salt", json={"email": email}, timeout=timeout
        ) as resp:
            if resp.status != 200:
                raise Exception("Failed login (salt)")
            salt = base64.b64decode((await resp.json())["salt"], validate=True)
        # 2. login with best private keyring
        for private_kr_cls, password in AbstractPrivateKeyring.flattened_password_forms_with_cls(
            PRIVATE_KEYRING_CLASSES, password
        ):
            private_kr = create_account_login_private_keyring(private_kr_cls, password, salt)
            request = create_account_login_request_params(private_kr, email, server_session_key, nano=True)
            if otp_salt is not None:
                request["otpSalt"] = otp_salt
            if tfa_code is not None:
                request["2faCode"] = tfa_code
            async with self.post_auto_csrf(
                f"https://{self._d}{self._p}r/login",
                json=request,
                timeout=None if timeout is None else (t + timeout) - time.perf_counter(),
            ) as resp:
                if resp.status != 200:
                    logger.warning("Login response is unsuccessful: %s", resp.status)
                    continue
                login_data = await resp.json()
                if not isinstance(login_data, dict):
                    logger.warning("Login data response is incorrect: %s", login_data)
                    continue
                if two_factor := login_data.get("2FA"):
                    raise TwoFactorError(gateway_to_control(two_factor))
                for k in [
                    "encrypted_master_key",
                    "encrypting_secret_ec",
                    "signing_secret_ec",
                    "encrypted_session_key",
                ]:
                    login_data[k] = base64.b64decode(login_data[k], validate=True)
                if "public_keys" in login_data:
                    for k, v in login_data["public_keys"].items():
                        login_data["public_keys"][k] = base64.b64decode(v, validate=True)
                else:
                    login_data["public_key"] = base64.b64decode(login_data["public_key"], validate=True)
                break
        else:
            raise LoginFailed("Failed login (auth)")
        del password  # no longer needed
        del salt  # no longer needed
        # 3. decrypt the account's keyring and session key
        self_kr = create_account_login_keyring(
            NanoSelfAccountKeyring,
            private_kr,
            login_data["account_id"],
            login_data["encrypted_master_key"],
            login_data["encrypting_secret_ec"],
            login_data["signing_secret_ec"],
        )
        session_key = create_account_login_session(
            self_kr, login_data["encrypted_session_key"], login_data.get("public_key"), login_data.get("public_keys")
        )
        del login_data  # no longer needed
        del private_kr  # no longer needed
        # 4.load session key (auth token) into session
        cooks = SimpleCookie({"k": base64.b64encode(session_key).decode("ascii")})
        cooks["k"]["domain"] = self._d
        cooks["k"]["path"] = "/"
        cooks["k"]["secure"] = True
        # other attributes are not applicable really; just mirror browser behaviour
        cooks["k"]["httponly"] = False
        cooks["k"]["samesite"] = "strict"
        self._s.cookie_jar.update_cookies(cooks)
        # 5. save authentication to local storage with combined session encryption
        self._cjs.save_session_cookies()
        self._write_local_storage(server_session_key, self_kr)
        # 6. return keys for usage
        return self_kr
