import base64
import os
import time
from collections import deque
from typing import Any, Callable

import ttldict2
from clarabot_nano_common.common.protocol.errors import (
    NANO_REQUEST_ERROR_ADMIN_PASSWORD_MISSING,
    NANO_REQUEST_ERROR_ADMIN_PASSWORD,
    NANO_REQUEST_ERROR_TOO_MANY_FAILURES,
)

from clarabot_nano.common.config.extract.admin_password import AdminPasswordExtractedConfig


class AdminAuthenticationError(Exception):
    def __init__(self, code: int, message: str):
        Exception.__init__(self, code, message)

    @property
    def code(self) -> int:
        return self.args[0]

    @property
    def message(self) -> str:
        return self.args[1]


class GatewayAdminSessions(object):
    """
    The owner's admin-password policy is enforced on the gateway to provide reliable rate-limiting and session-control.
    """

    def __init__(self, get_extracted_config: Callable[[], AdminPasswordExtractedConfig]):
        self._get_extracted_config = get_extracted_config
        self._session_tokens = ttldict2.TTLDict(60.0 * 3)
        # rate limits are global for admin authentication
        self._password_fails = deque()
        self._token_fails = deque()

    def get_token_ttl(self) -> int:
        return int(self._session_tokens.ttl_seconds)

    def get_missing_error_code(self) -> int:
        if self._get_extracted_config().can_setup_password():
            return NANO_REQUEST_ERROR_ADMIN_PASSWORD_MISSING
        return NANO_REQUEST_ERROR_ADMIN_PASSWORD

    def _get_deque_fails(self, d: deque, ttl: int) -> int:
        t = time.perf_counter() - ttl
        while d and d[0] < t:
            d.popleft()
        return len(d)

    def create_session(self, admin_password: Any) -> str:
        """
        Called by a specific request handler in gateway.
        """
        if self._get_deque_fails(self._password_fails, ttl=60) >= 5:
            raise AdminAuthenticationError(NANO_REQUEST_ERROR_TOO_MANY_FAILURES, "Too many admin password failures")
        if self._get_extracted_config().check_admin_password(admin_password):
            while True:
                token = base64.b64encode(os.urandom(24)).decode("ascii")
                if token not in self._session_tokens:
                    break
            self._session_tokens[token] = 1
            return token
        if admin_password is not None:
            # Do not could as failure when no password is provided by the client. This would work as-is if the
            # admin-policy was "allow" and this case is also useful for the client to poll what password is needed.
            self._password_fails.append(time.perf_counter())
        raise AdminAuthenticationError(self.get_missing_error_code(), "Admin password invalid")

    def validate_session(self, session_token: Any) -> int:
        """
        Called by the gateway request processing.
        """
        if self._get_deque_fails(self._token_fails, ttl=60) >= 8:
            raise AdminAuthenticationError(NANO_REQUEST_ERROR_TOO_MANY_FAILURES, "Too many admin session failures")
        if isinstance(session_token, str):
            self._session_tokens.drop_old_items()
            try:  # get ttl-entry directly to read the expiration value
                entry = dict.__getitem__(self._session_tokens, session_token)
                return max(1, int(entry.expires - time.perf_counter()))
            except KeyError:
                pass
        self._token_fails.append(time.perf_counter())
        raise AdminAuthenticationError(self.get_missing_error_code(), "Admin session expired or invalid")

    def touch_session(self, session_token: str) -> None:
        """
        Called by the gateway to extend the validity of the session when a handler specifies such.
        """
        self._session_tokens.get(session_token, touch=True)

    def reset(self) -> None:
        self._session_tokens.clear()
