# coding: utf-8
import asyncio
import base64
import random
import re
import string
import datetime

from sqliteclient.data.Models import TableApiTokens, ViewUsersApiTokens, TablePasswords
from . import sqliteClient
from typing import Dict, List, Tuple, Optional  # noqa: F401
from ..utils.password import (
    generateSalt,
    hashPasswordPbkdf2,
    verifyStoredPassword,
    passwordNeedsUpgrade,
    LOGIN_FAILURE_DELAY,
)

from fastapi import (  # noqa: F401
    APIRouter,
    Body,
    Cookie,
    Depends,
    Form,
    Header,
    Path,
    Query,
    Response,
    Security,
    status,
    HTTPException,
)

from server.models.extra_models import TokenModel  # noqa: F401
from server.models.auth_request_model import AuthRequestModel
from server.models.authenticate_post201_response import AuthenticatePost201Response
from server.models.error import Error
from server.data.definitions import ErrorTypes, Messages, TokenDefinitions, API_SUB_PATH

router = APIRouter(prefix=API_SUB_PATH)


@router.post(
    "/authenticate",
    responses={
        201: {
            "model": AuthenticatePost201Response,
            "description": "Authentication succeeded and API Token is created.",
        },
        400: {"model": Error, "description": "Bad request."},
        401: {"model": Error, "description": "Password is wrong."},
        404: {"model": Error, "description": "User not found."},
    },
    tags=["authentication"],
    response_model_by_alias=True,
)
async def authenticate_post(
    auth_request_model: AuthRequestModel = Body(None, description="Requestbody for the POST action.")
) -> AuthenticatePost201Response:
    """User Authentication."""
    # -------------------------------------------- Check fields aren't empty ------------------------------------------

    if auth_request_model is None:
        errorResponse = Error(
            status=status.HTTP_400_BAD_REQUEST,
            type=ErrorTypes.REQUEST_IS_EMPTY.value,
            message=Messages.REQUEST_IS_EMPTY.value,
        )
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=errorResponse.dict())

    # -------------------------------------------- Load database --------------------------------------------

    # Load data from the database into the client, if this didn't happen
    if not sqliteClient.wasLoaded:
        sqliteClient.startLoadTablesTask()
        await sqliteClient.loadTablesTask

    # -------------------------------------------- Injection test --------------------------------------------

    for string in [auth_request_model.username, auth_request_model.password]:
        if hasSqlInjectionCharacters(string):
            errorResponse = Error(
                status=status.HTTP_400_BAD_REQUEST,
                type=ErrorTypes.INJECTION_DETECTED.value,
                message=Messages.INJECTION_DETECTED.value,
            )
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=errorResponse.dict())

    # -------------------------------------------- User authentication --------------------------------------------

    # Check if the user does exist
    if not sqliteClient.checkUserExists(auth_request_model.username):
        await asyncio.sleep(LOGIN_FAILURE_DELAY)  # brute force protection, delay based

        errorResponse = Error(
            status=status.HTTP_404_NOT_FOUND,
            type=ErrorTypes.USER_NOT_FOUND.value,
            message=Messages.USER_NOT_FOUND.value,
        )
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=errorResponse.dict())

    # Check if the password does match
    matchingUsers = sqliteClient.getUsersPasswords(whereUser=auth_request_model.username)
    matchingUserData = matchingUsers[0] if matchingUsers else None

    if matchingUserData is None:
        errorResponse = Error(
            status=status.HTTP_401_UNAUTHORIZED, type=ErrorTypes.WRONG_PASS.value, message=Messages.PASS_NOT_MATCH.value
        )
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=errorResponse.dict())

    passwordBytes = auth_request_model.password.encode("utf-8")

    if not verifyStoredPassword(passwordBytes, matchingUserData.password, matchingUserData.seed):
        await asyncio.sleep(LOGIN_FAILURE_DELAY)  # brute force protection, delay based

        errorResponse = Error(
            status=status.HTTP_401_UNAUTHORIZED, type=ErrorTypes.WRONG_PASS.value, message=Messages.PASS_NOT_MATCH.value
        )
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=errorResponse.dict())

    # Upgrade after successful verification if still in a legacy format
    if passwordNeedsUpgrade(matchingUserData.password):
        upgradedPasswordHash = hashPasswordPbkdf2(passwordBytes, generateSalt())
        upgradedPasswordEntry = TablePasswords(
            passwordId=matchingUserData.passwordId,
            users_userId=matchingUserData.userId,
            password=upgradedPasswordHash,
            seed=None,
        )
        await sqliteClient.setPassword(upgradedPasswordEntry)

    # -------------------------------------------- Api token handling --------------------------------------------

    # Retrieve the session tokens
    sessionTokenRows = sqliteClient.getUsersApiTokens(whereUser=auth_request_model.username, getSessionToken=True)
    token = ""

    # Check session token does exist
    if len(sessionTokenRows) > 0:
        sessionTokenRow = sessionTokenRows[0]
        tokenIsValid = isSessionTokenValid(sessionTokenRow=sessionTokenRow)

        if not tokenIsValid:
            # Todo: add try catch
            await deleteToken(auth_request_model, isSessionToken=True)
            token = await createNewToken(requestedUser=auth_request_model.username)
        else:
            token = sessionTokenRow.token

    # When the api token doesnt exist
    elif not len(sessionTokenRows) > 0:
        token = await createNewToken(requestedUser=auth_request_model.username)

    # -------------------------------------------- uGroups handling --------------------------------------------

    listOfGroups = []
    uGroups = sqliteClient.getUsersGroups(whereUser=auth_request_model.username)

    for uGroupRow in uGroups:
        if uGroupRow.groupName not in listOfGroups:
            listOfGroups.append(uGroupRow.groupName)

    # -------------------------------------------- Building the return --------------------------------------------

    # encodedToken = base64.b64encode(token.encode()).decode()
    encodedGroups = base64.b64encode(str(listOfGroups).encode()).decode()
    return AuthenticatePost201Response(uGroups=encodedGroups, apiToken=token)


async def generateApiKey(tokenLength: int) -> str:
    """
    Generates a random string with chars as a token.

    :param tokenLength: The length of the random token will be created.
    :return: The Api token.
    """
    # Define the set of characters to choose from
    characters = string.ascii_letters + string.digits + string.punctuation

    # Remove unwanted characters
    unwantedCharacters = [
        "'",
        ";",
        "-",
        "/",
        "*",
        "xp_",
        '"',
        "`",
        "#",
        "|",
        "\\",
        "<",
        ">",
        "%",
        "&",
        "(",
        ")",
        "=",
        "[",
        "]",
        "{",
        "}",
        "@",
        "$",
        "!",
        "~",
        "^",
        ",",
        ".",
        "?",
        ":",
        "_",
    ]

    characters = "".join(c for c in characters if c not in unwantedCharacters)

    # Generate a random apiToken using the defined characters
    apiToken = "".join(random.choice(characters) for _ in range(tokenLength))

    return apiToken


async def createNewToken(
    requestedUser: str,
    isSessionToken: bool = True,
    tokenName: str = "",
    validityStart: Optional[str] = None,
    validityEnd: Optional[str] = None,
) -> str:
    """
    Creates for the user where is requesting the access, a new api token

    :param requestedUser: The user is requesting the access.
    :return: The api token for the access.
    """
    # Arrange new token data
    userData = sqliteClient.getUsersPasswords(whereUser=requestedUser)[0]
    newToken = await generateApiKey(tokenLength=TokenDefinitions.TOKEN_LENGTH.value)

    if not validityStart or not validityEnd:
        validityStart, validityEnd = await createUTCTimeStrings(tokenLifetime=TokenDefinitions.TOKEN_LIFETIME.value)

    # Arrange the model for the database
    newTokenRow = TableApiTokens(
        users_userId=userData.userId,
        token=newToken,
        validityStart=validityStart,
        validityEnd=validityEnd,
        historyState=TokenDefinitions.HISTORY_STATE_CURRENT.value,
        isSessionToken=isSessionToken,
        tokenName=tokenName,
    )
    createdToken = await sqliteClient.createSessionToken(username=userData.userName, newTokenRow=newTokenRow)

    return createdToken


async def deleteToken(requestedUser: AuthRequestModel, isSessionToken: bool = True) -> None:
    """
    Deletes the session token of the requested user.

    :param requestedUser: The user is requesting the access.
    """
    userData = sqliteClient.getUsersPasswords(whereUser=requestedUser.username)[0]
    await sqliteClient.deleteApiToken(userData=userData, isSessionToken=isSessionToken)


async def createUTCTimeStrings(tokenLifetime: int) -> Tuple[str, str]:
    """
    Generates the UTC time strings for created and expires.

    :param tokenLifetime: The time the token doesn't need to be updated.
    :return: The timestamps for created and expires.
    """
    # Get the current UTC datetime
    currentTime = datetime.datetime.utcnow()

    # Add lifeTime to the current time
    expiresTime = currentTime + datetime.timedelta(days=tokenLifetime)

    # Format the datetime objects as strings
    createdStr = currentTime.strftime("%Y-%m-%d %H:%M:%S")
    expiresStr = expiresTime.strftime("%Y-%m-%d %H:%M:%S")

    return createdStr, expiresStr


def isSessionTokenValid(sessionTokenRow: ViewUsersApiTokens) -> bool:
    """
    Checks if the given database entry is valid.

    :param sessionTokenRow: The row with the api token where needs to be checked.
    :return: True if the row is valid, else False.
    """
    # Get the current time
    currentTime = datetime.datetime.now()

    # Convert strings to datetime objects
    createdTime = datetime.datetime.strptime(sessionTokenRow.validityStart, "%Y-%m-%d %H:%M:%S")
    expiresTime = datetime.datetime.strptime(sessionTokenRow.validityEnd, "%Y-%m-%d %H:%M:%S")

    return createdTime <= currentTime <= expiresTime


def hasSqlInjectionCharacters(inputString: str) -> bool:
    """
    Checks a string if it contains some sql injection characters or keywords.

    :param inputString: The string for the check.
    :return: True if the string contains bad character.
    """
    # Define a regular expression pattern for potentially harmful characters and SQL keywords
    sqlInjectionPattern = re.compile(
        r'[;<>\'"%&()=]|(\bunion\b|\bselect\b|\binsert\b|\bupdate\b|\bdelete\b|\bdrop\b|\balter\b|\bcreate\b)',
        re.IGNORECASE,
    )

    # Check if the input string matches the pattern
    match = re.search(sqlInjectionPattern, inputString)

    return bool(match)
