from __future__ import annotations

import io
from copy import copy
from hashlib import sha1
import logging
import secrets
from dataclasses import dataclass
from typing import Optional, Dict, AsyncGenerator, Union, Tuple, Sequence

from mysql_mimic.types import read_str_null
from mysql_mimic.utils import xor

logger = logging.getLogger(__name__)


# Many authentication plugins don't need to send any sort of challenge/nonce.
FILLER = b"0" * 20 + b"\x00"


@dataclass
class Forbidden:
    msg: Optional[str] = None


@dataclass
class Success:
    authenticated_as: str


@dataclass
class User:
    name: str
    auth_string: Optional[str] = None
    auth_plugin: Optional[str] = None

    # For plugins that support a primary and secondary password
    # This is helpful for zero-downtime password rotation
    old_auth_string: Optional[str] = None


@dataclass
class AuthInfo:
    username: str
    data: bytes
    user: User
    connect_attrs: Dict[str, str]
    client_plugin_name: Optional[str]
    handshake_auth_data: Optional[bytes]
    handshake_plugin_name: str

    def copy(self, data: bytes) -> AuthInfo:
        new = copy(self)
        new.data = data
        return new


Decision = Union[Success, Forbidden, bytes]
AuthState = AsyncGenerator[Decision, AuthInfo]


class AuthPlugin:
    """
    Abstract base class for authentication plugins.
    """

    name = ""
    client_plugin_name: Optional[str] = None  # None means any

    async def auth(self, auth_info: Optional[AuthInfo] = None) -> AuthState:
        """
        Create an async generator that drives the authentication lifecycle.

        This should either yield `bytes`, in which case an AuthMoreData packet is sent to the client,
        or a `Success` or `Forbidden` instance, in which case authentication is complete.

        Args:
             auth_info: This is None if authentication is starting from the optimistic handshake.
        """
        yield Forbidden()

    async def start(
        self, auth_info: Optional[AuthInfo] = None
    ) -> Tuple[Decision, AuthState]:
        state = self.auth(auth_info)
        data = await state.__anext__()
        return data, state


class GullibleAuthPlugin(AuthPlugin):
    """
    Custom plugin that naively accepts whatever username a client provides.
    """

    name = "mysql_mimic_gullible"

    async def auth(self, auth_info: Optional[AuthInfo] = None) -> AuthState:
        if not auth_info:
            auth_info = yield FILLER
        yield Success(authenticated_as=auth_info.username)


class AbstractClearPasswordAuthPlugin(AuthPlugin):
    """
    Abstract class for implementing the server-side of the standard client plugin "mysql_clear_password".
    """

    name = "abstract_mysql_clear_password"
    client_plugin_name = "mysql_clear_password"

    async def auth(self, auth_info: Optional[AuthInfo] = None) -> AuthState:
        if not auth_info:
            auth_info = yield FILLER

        r = io.BytesIO(auth_info.data)
        password = read_str_null(r).decode()
        authenticated_as = await self.check(auth_info.username, password)
        if authenticated_as is not None:
            yield Success(authenticated_as)
        else:
            yield Forbidden()

    async def check(self, username: str, password: str) -> Optional[str]:
        return username


class NativePasswordAuthPlugin(AuthPlugin):
    """
    Standard plugin that uses a password hashing method.

    The client hashed the password using a nonce provided by the server, so the
    password can't be snooped on the network.

    Furthermore, thanks to some clever hashing techniques, knowing the hash stored in the
    user database isn't enough to authenticate as that user.
    """

    name = "mysql_native_password"
    client_plugin_name = "mysql_native_password"

    async def auth(self, auth_info: Optional[AuthInfo] = None) -> AuthState:
        if (
            auth_info
            and auth_info.handshake_plugin_name == self.name
            and auth_info.handshake_auth_data
        ):
            # mysql_native_password can reuse the nonce from the initial handshake
            nonce = auth_info.handshake_auth_data.rstrip(b"\x00")
        else:
            nonce = secrets.token_bytes(20)
            # The mysql command line client expects a null terminating byte
            auth_info = yield nonce + b"\x00"

        user = auth_info.user
        if self.password_matches(user=user, scramble=auth_info.data, nonce=nonce):
            yield Success(user.name)
        else:
            yield Forbidden()

    def empty_password_quickpath(self, user: User, scramble: bytes) -> bool:
        return not scramble and not user.auth_string

    def password_matches(self, user: User, scramble: bytes, nonce: bytes) -> bool:
        return (
            self.empty_password_quickpath(user, scramble)
            or self.verify_scramble(user.auth_string, scramble, nonce)
            or self.verify_scramble(user.old_auth_string, scramble, nonce)
        )

    def verify_scramble(
        self, auth_string: Optional[str], scramble: bytes, nonce: bytes
    ) -> bool:
        # From docs,
        # response.data should be:
        #   SHA1(password) XOR SHA1("20-bytes random data from server" <concat> SHA1(SHA1(password)))
        # auth_string should be:
        #   SHA1(SHA1(password))
        sha1_sha1_password = bytes.fromhex(auth_string or "")
        sha1_sha1_with_nonce = sha1(nonce + sha1_sha1_password).digest()
        rcvd_sha1_password = xor(scramble, sha1_sha1_with_nonce)
        return sha1(rcvd_sha1_password).digest() == sha1_sha1_password

    @classmethod
    def create_auth_string(cls, password: str) -> str:
        return sha1(sha1(password.encode("utf-8")).digest()).hexdigest()


class NoLoginAuthPlugin(AuthPlugin):
    """
    Standard plugin that prevents all clients from direct login.

    This is useful for user accounts that can only be accessed by proxy authentication.
    """

    name = "mysql_no_login"

    async def auth(self, auth_info: Optional[AuthInfo] = None) -> AuthState:
        if not auth_info:
            _ = yield FILLER
        yield Forbidden()


class IdentityProvider:
    """
    Abstract base class for an identity provider.

    An identity provider tells the server with authentication plugins to make
    available to clients and how to retrieve users.
    """

    def get_plugins(self) -> Sequence[AuthPlugin]:
        return [NativePasswordAuthPlugin(), NoLoginAuthPlugin()]

    async def get_user(self, username: str) -> Optional[User]:
        return None

    def get_default_plugin(self) -> AuthPlugin:
        return self.get_plugins()[0]

    def get_plugin(self, name: str) -> Optional[AuthPlugin]:
        try:
            return next(p for p in self.get_plugins() if p.name == name)
        except StopIteration:
            return None


class SimpleIdentityProvider(IdentityProvider):
    """
    Simple identity provider implementation that naively accepts whatever username a client provides.
    """

    def get_plugins(self) -> Sequence[AuthPlugin]:
        return [GullibleAuthPlugin()]

    async def get_user(self, username: str) -> Optional[User]:
        return User(name=username, auth_plugin=GullibleAuthPlugin.name)
