import hashlib
import logging
import time
import string
import secrets
import pyotp

from argon2 import PasswordHasher
from config import Config
from flask import request, redirect, jsonify, make_response, abort, g
from functools import wraps

from app.models.user import User
from app.models.placeservers import PlaceServer
from app.models.gameservers import GameServer
from app.extensions import db, redis_controller, get_remote_address

config = Config()

def ValidateToken( token : str ) -> bool:
    TokenInfo = redis_controller.get("authtoken_" + token)
    if TokenInfo is None:
        return False
    
    # Token Info Format
    # userid|created|expiry|ip
    TokenInfo = TokenInfo.split("|")
    if len(TokenInfo) != 4:
        redis_controller.delete("authtoken_" + token)
        return False
    
    if int(TokenInfo[2]) < int(time.time()):
        redis_controller.delete("authtoken_" + token)
        return False
    
    return True

def GetAuthenticatedUser( token : str ) -> User:
    AuthTokenInfo = GetTokenInfo(token)
    if AuthTokenInfo is None:
        return None
    return User.query.filter_by(id=int(AuthTokenInfo[0])).first()

def GetTokenInfo( token : str ) -> list:
    TokenInfo = redis_controller.get("authtoken_" + token)
    if TokenInfo is None:
        return None
    
    # Token Info Format
    # userid|created|expiry|ip
    TokenInfo = TokenInfo.split("|")
    if len(TokenInfo) != 4:
        redis_controller.delete("authtoken_" + token)
        return None
    
    if int(TokenInfo[2]) < int(time.time()):
        redis_controller.delete("authtoken_" + token)
        return None
    
    return TokenInfo

def CreateToken( userid : int , ip, expireIn : int = (60 * 60 * 24 * 31)) -> str:
    Token = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(512))

    # Token Info Format
    # userid|created|expiry|ip
    
    TokenInfo = str(userid) + "|" + str(int(time.time())) + "|" + str(int(time.time()) + expireIn) + "|" + ip
    redis_controller.set("authtoken_" + Token, TokenInfo, ex = expireIn)
    
    return Token

def isAuthenticated():
    if ".ROBLOSECURITY" not in request.cookies:
        return False
    if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
        return False
    return True

CRAWLER_BOT_USERAGENTS = (
    "discordbot",
    "slackbot",
    "telegrambot",
    "twitterbot",
    "facebookexternalhit",
    "whatsapp",
    "linkedinbot",
    "redditbot",
    "embedly",
)

def IsCrawlerBot() -> bool:
    UserAgentString = (request.user_agent.string or "").lower()
    return any(bot in UserAgentString for bot in CRAWLER_BOT_USERAGENTS)

def authenticated_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if ".ROBLOSECURITY" not in request.cookies:
            return redirect("/login")
        if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
            RedirectResponse = make_response(redirect("/login"))
            RedirectResponse.set_cookie(".ROBLOSECURITY", expires=0)
            return RedirectResponse
        return f(*args, **kwargs)
    return decorated_function

def discord_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if ".ROBLOSECURITY" not in request.cookies:
            return redirect("/login")
        if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
            RedirectResponse = make_response(redirect("/login"))
            RedirectResponse.set_cookie(".ROBLOSECURITY", expires=0)
            return RedirectResponse

        CurrentUser = GetCurrentUser()
        if CurrentUser is None:
            return redirect("/login")

        from app.models.linked_discord import LinkedDiscord
        LinkedDiscordObj = LinkedDiscord.query.filter_by(user_id=CurrentUser.id).first()
        if LinkedDiscordObj is None:
            return redirect("/settings?discord_required=1")

        return f(*args, **kwargs)
    return decorated_function

def authenticated_or_bot_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if IsCrawlerBot():
            return f(*args, **kwargs)
        if ".ROBLOSECURITY" not in request.cookies:
            return redirect("/login")
        if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
            RedirectResponse = make_response(redirect("/login"))
            RedirectResponse.set_cookie(".ROBLOSECURITY", expires=0)
            return RedirectResponse
        return f(*args, **kwargs)
    return decorated_function

def authenticated_required_api(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if ".ROBLOSECURITY" not in request.cookies:
            return jsonify({"success": False, "message": "You are not logged in"}), 401
        if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
            ErrorResponse = make_response(jsonify({"success": False, "message": "You are not logged in"}), 401)
            ErrorResponse.set_cookie(".ROBLOSECURITY", expires=0)
            return ErrorResponse
        
        return f(*args, **kwargs)
    return decorated_function

def authenticated_client_endpoint(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if request.cookies.get("Syntax-Session-Id", type = str ) is not None:
            DataSections = request.cookies.get("Syntax-Session-Id", type = str )
        else:
            return jsonify({"success": False, 'error': 'Missing Headers'}), 401
        
        if len(DataSections) != 9:
            return jsonify({"success": False, 'error': 'Invalid Headers'}), 401
        
        AuthToken = DataSections[8]
        if not ValidateToken(AuthToken):
            return jsonify({"success": False, 'error': 'Invalid Token'}), 401
        
        PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=str(DataSections[1])).first()
        if PlaceServerObj is None:
            invalidateToken(AuthToken)
            return jsonify({"success": False, 'error': 'Invalid Token'}), 401

        return f(*args, **kwargs)
    return decorated_function

def gameserver_authenticated_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if "Roblox/" not in request.user_agent.string:
            abort(404)
        RemoteAddress = get_remote_address()
        GameServerObj : GameServer = GameServer.query.filter_by( serverIP = RemoteAddress ).first()
        if GameServerObj is None:
            abort(404)
        requesterAccessKey = request.headers.get( key = "AccessKey", type = str, default = "")
        if "UserRequest" in requesterAccessKey:
            logging.warning(f"GameServer {RemoteAddress} - UserRequest access key used for {request.url}")
            abort(404)
        return f(*args, **kwargs)
    return decorated_function

def gameserver_accesskey_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if "AccessKey" not in request.headers:
            abort(404)
        RequesterAccessKey = request.headers.get( key = "AccessKey", type = str, default = "")
        RemoteAddress = get_remote_address()
        GameServerObj : GameServer = GameServer.query.filter_by( serverIP = RemoteAddress, accessKey = RequesterAccessKey ).first()
        if GameServerObj is None:
            abort(404)
        return f(*args, **kwargs)
    return decorated_function

def invalidateToken(token : str):
    if token is not None:
        redis_controller.delete("authtoken_" + token)

def Validate2FACode( userid : int, code : str ) -> bool:
    from app.pages.settings.settings import generate_secret_key_from_string
    """
        Validates a 2FA code for a user

        :param userid: The user id to validate the code for
        :param code: The code to validate

        :returns: bool (True if correct, False if not)
    """
    user : User = User.query.filter_by(id=userid).first()
    if user is None:
        return False
    if user.TOTPEnabled == False:
        return False
    totp = pyotp.TOTP(generate_secret_key_from_string(str(user.id) + config.FLASK_SESSION_KEY))
    return totp.verify(code)
    
def GetCurrentUser() -> User:
    """
    Gets the current user from the request

    :returns: User (User object if logged in, None if not)
    """

    def _get_user() -> User:
        if ".ROBLOSECURITY" not in request.cookies:
            if "Syntax-Session-Id" not in request.cookies:
                return None
            else:
                DataSections = request.cookies["Syntax-Session-Id"].split("|")
            if len(DataSections) != 9:
                return None
            
            AuthToken = DataSections[8]
            if not ValidateToken(AuthToken):
                return None

            PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=str(DataSections[1])).first()
            if PlaceServerObj is None:
                invalidateToken(AuthToken)
                return None
            
            AuthTokenInfo = GetTokenInfo(AuthToken)
            return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
        if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
            return None
        AuthTokenInfo = GetTokenInfo(request.cookies[".ROBLOSECURITY"])
        if AuthTokenInfo is None:
            return None
        return User.query.filter_by(id=int(AuthTokenInfo[0])).first()

    if not hasattr(g, 'current_authenticated_user'):
        g.current_authenticated_user = _get_user()
    return g.current_authenticated_user


def _GetArgonSalt( UserObj : User ) -> bytes:
    return (config.FLASK_SESSION_KEY + str(UserObj.id)).encode('utf-8')
def _GetPasswordHasher() -> PasswordHasher:
    return PasswordHasher(
        time_cost=16,
        memory_cost=2**14,
        parallelism=2,
        hash_len=32,
        salt_len=16
    )

def VerifyPassword( UserObj : User, password : str ) -> bool:
    """
        Verifies the password of the given user
    
        :param UserObj: The user object to verify the password of
        :param password: The password to verify

        :returns: bool (True if correct, False if not)
    """
    if config.SWITCH_TO_ARGON_PASSWORD_HASH: # Switch this when we migrate to argon
        argon_ph = _GetPasswordHasher()
        if not UserObj.password.startswith("$argon2id"): # Handle old passwords in sha512
            isCorrect = hashlib.sha512(password.encode('utf-8')).hexdigest() == UserObj.password
            if isCorrect:
                UserObj.password = PasswordHasher().hash(password, salt = _GetArgonSalt(UserObj) )
                logging.info(f"User {UserObj.username} [{UserObj.id}] migrated to argon2id password hash")
                db.session.commit()
            return isCorrect
        
        try:
            return argon_ph.verify(UserObj.password, password)
        except:
            return False
    else:
        return hashlib.sha512(password.encode('utf-8')).hexdigest() == UserObj.password
    
def SetPassword( UserObj : User, password : str ):
    """
        Sets the password of the given user

        :param UserObj: The user object to set the password of
        :param password: The password to set

        :returns: bool (True if successful, False if not)
    """
    if config.SWITCH_TO_ARGON_PASSWORD_HASH: # Switch this when we migrate to argon
        argon_ph = _GetPasswordHasher()
        UserObj.password = argon_ph.hash(password, salt = _GetArgonSalt(UserObj) )
    else:
        UserObj.password = hashlib.sha512(password.encode('utf-8')).hexdigest()
    
    db.session.commit()
    return True
