from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
from functools import wraps
from app.extensions import db, redis_controller, csrf, limiter
import uuid
import json
import base64
import re
from datetime import datetime, timedelta, timezone
from app.enums.AssetType import AssetType
import hashlib
import os
import requests
import logging
import secrets
import random
import time
import requests
import threading
import string
from sqlalchemy import or_, and_, func
from config import Config
config = Config()

from app.routes.asset import migrateAsset
from app.util import auth, assetversion, discord, redislock, announcement
from app.services import economy, gameserver_comm
from app.models.admin_permissions import AdminPermissions
from app.models.user import User
from app.models.gameservers import GameServer
from app.models.placeservers import PlaceServer
from app.models.placeserver_players import PlaceServerPlayer
from app.models.fflag_group import FflagGroup
from app.models.fflag_value import FflagValue
from app.models.asset import Asset
from app.models.asset_version import AssetVersion
from app.models.asset_moderation_link import AssetModerationLink
from app.models.asset_thumbnail import AssetThumbnail
from app.models.place_icon import PlaceIcon
from app.models.past_usernames import PastUsername
from app.models.usereconomy import UserEconomy
from app.models.login_records import LoginRecord
from app.models.place import Place
from app.models.linked_discord import LinkedDiscord
from app.models.user_ban import UserBan
from app.models.giftcard_key import GiftcardKey
from app.models.invite_key import InviteKey
from app.models.groups import Group, GroupIcon
from app.models.userassets import UserAsset
from app.models.user_avatar_asset import UserAvatarAsset
from app.models.user_hwid_log import UserHWIDLog
from app.models.game_session_log import GameSessionLog
from app.models.user_transactions import UserTransaction
from app.models.moderator_note import ModeratorNote
from app.models.universe import Universe
from app.models.limited_item_transfers import LimitedItemTransfer
from app.models.user_email import UserEmail
from app.models.messages import Message
from app.models.friend_request import FriendRequest
from app.models.friend_relationship import FriendRelationship
from app.models.follow_relationship import FollowRelationship
from app.models.groups import GroupMember, GroupJoinRequest, GroupWallPost
from app.models.user_avatar import UserAvatar
from app.models.user_thumbnail import UserThumbnail
from app.enums.GiftcardType import GiftcardType
from app.enums.BanType import BanType
from app.enums.TransactionType import TransactionType
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
from app.routes.jobreporthandler import EvictPlayer
from app.routes.thumbnailer import TakeThumbnail, TakeUserThumbnail
from app.pages.admin.permissionsdefinition import PermissionsDefinition
from app.pages.messages.messages import CreateSystemMessage
from app.services import invitekeys as invitekeyservice

def GetCreatorOfAsset( AssetObj : Asset ) -> User | Group | None:
    if AssetObj.creator_type == 0:
        return User.query.filter_by(id=AssetObj.creator_id).first()
    elif AssetObj.creator_type == 1:
        return Group.query.filter_by(id=AssetObj.creator_id).first()
    return None

def AdminPermissionRequired(permission):
    UserObj : User = auth.GetCurrentUser()
    UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id, permission=permission).first()
    if UserAdminPermission is None:
        abort(403)        

def HasAdminPermission(permission) -> bool:
    UserObj : User = auth.GetCurrentUser()
    UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id, permission=permission).first()
    if UserAdminPermission is None:
        return False
    return True

def IsUserAnAdministrator( UserObj : User, AvoidCache : bool = False) -> bool:
    if not AvoidCache:
        CachedResult = redis_controller.get(f"IsUserAnAdministrator:Lookup:{UserObj.id}")
        if CachedResult is not None:
            try:
                return CachedResult == "1"
            except:
                pass
    isUserAdministrator = AdminPermissions.query.filter_by(userid=UserObj.id).first() is not None
    redis_controller.set(f"IsUserAnAdministrator:Lookup:{UserObj.id}", int(isUserAdministrator), ex = 60)

    return isUserAdministrator

def GetAmountOfPendingAssets( AvoidCache : bool = False ) -> int:
    if not AvoidCache:
        CachedResult = redis_controller.get("GetAmountOfPendingAssets:Lookup")
        if CachedResult is not None:
            try:
                return int(CachedResult)
            except:
                pass
    PendingAssetsCount = Asset.query.filter_by(moderation_status=1).count() + PlaceIcon.query.filter_by(moderation_status=1).count() + AssetThumbnail.query.filter_by(moderation_status=1).count() + GroupIcon.query.filter_by(moderation_status=1).count()
    redis_controller.set("GetAmountOfPendingAssets:Lookup", str(PendingAssetsCount), ex = 10)

    return PendingAssetsCount

def MustBeAdmin(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not auth.isAuthenticated():
            return redirect("/login")
        UserObj : User = auth.GetCurrentUser()
        UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id).first()
        if UserAdminPermission is None:
            return redirect("/home")
        return f(*args, **kwargs)
    return decorated_function

def GetAdminPermissions():
    # Get admin permissions for the current user
    # and returns them in a array
    UserObj : User = auth.GetCurrentUser()
    UserAdminPermissions = AdminPermissions.query.filter_by(userid=UserObj.id).all()
    if UserAdminPermissions is None:
        return []
    UserAdminPermissionsArray = []
    for UserAdminPermission in UserAdminPermissions:
        UserAdminPermissionsArray.append(UserAdminPermission.permission)
    return UserAdminPermissionsArray

AdminRoute = Blueprint('admin', __name__, url_prefix='/admin')

@AdminRoute.before_request
def before_request():
    if not auth.isAuthenticated():
        return redirect("/login")
    UserObj = auth.GetCurrentUser()  # Fixed line
    UserAdminPermission = AdminPermissions.query.filter_by(userid=UserObj.id).first()
    if UserAdminPermission is None:
        return redirect("/home")


@AdminRoute.route('/', methods=['GET'])
def admin():
    userPermissions = []
    for permission in GetAdminPermissions():
        if permission in PermissionsDefinition:
            userPermissions.append(PermissionsDefinition[permission])
    AuthenticatedUser : User = auth.GetCurrentUser()
    stats = {}
    stats['UsersPlaying'] = PlaceServerPlayer.query.count()
    stats['PlaceServersCount'] = PlaceServer.query.count()
    stats['UsersOnline'] = User.query.filter(User.lastonline > (datetime.utcnow() - timedelta(minutes=1))).count()
    stats['UsersSignedUpToday'] = User.query.filter(User.created > (datetime.utcnow() - timedelta(days=1))).count()
    stats['PendingAssets'] = Asset.query.filter_by(moderation_status=1).count() + PlaceIcon.query.filter_by(moderation_status=1).count() + AssetThumbnail.query.filter_by(moderation_status=1).count() + GroupIcon.query.filter_by(moderation_status=1).count()
    stats['SystemTime'] = datetime.utcnow()
    return render_template('admin/index.html', permissions = userPermissions, stats = stats, user = AuthenticatedUser)

@AdminRoute.route('/gameservers', methods=['GET'])
def gameservers():
    AdminPermissionRequired('GameServerManager')

    gameServers = GameServer.query.all()

    return render_template('admin/gameservers/index.html', gameservers = gameServers)

@AdminRoute.route('/gameservers/<serverid>', methods=['GET'])
def gameservers_view(serverid):
    AdminPermissionRequired('GameServerManager')

    gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
    if gameServer is None:
        return redirect("/admin/gameservers")

    return render_template('admin/gameservers/view.html', gameserver = gameServer)

@AdminRoute.route('/gameservers/<serverid>/delete', methods=['GET'])
def gameservers_delete(serverid):
    AdminPermissionRequired('GameServerManager')

    gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
    if gameServer is None:
        return redirect("/admin/gameservers")
    return render_template("/admin/gameservers/delete.html", gameserver = gameServer)

@AdminRoute.route('/gameservers/<serverid>/delete', methods=['POST'])
def gameservers_delete_post(serverid):
    AdminPermissionRequired('GameServerManager')

    gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
    if gameServer is None:
        return redirect("/admin/gameservers")

    db.session.delete(gameServer)
    db.session.commit()

    return redirect("/admin/gameservers")

@AdminRoute.route('/gameservers/<serverid>/edit', methods=['POST'])
def gameservers_edit(serverid):
    AdminPermissionRequired('GameServerManager')
    gameServer : GameServer = GameServer.query.filter_by(serverId=serverid).first()
    if gameServer is None:
        return redirect("/admin/gameservers")
    if request.form['name'] == "" or request.form['serverip'] == "" or request.form['serverport'] == "" or request.form['accesskey'] == "":
        flash("Please fill in all fields", "danger")
        return redirect("/admin/gameservers/" + serverid)
    
    gameServer.serverName = request.form['name']
    gameServer.serverIP = request.form['serverip']
    gameServer.serverPort = request.form['serverport']
    gameServer.accessKey = request.form['accesskey']
    gameServer.allowThumbnailGen = True if 'isThumbnailer' in request.form else False
    gameServer.allowGameServerHost = True if 'isGameHoster' in request.form else False
    db.session.commit()

    flash("Game server updated", "success")
    return redirect("/admin/gameservers/" + serverid)

@AdminRoute.route("/gameservers/<serverid>/refresh-accesskey", methods=['GET', 'POST'])
def gameservers_refresh_accesskey(serverid):
    AdminPermissionRequired('GameServerManager')
    GameServerObj : GameServer = GameServer.query.filter_by(serverId=serverid).first()
    if GameServerObj is None:
        return abort(404)
    
    if request.method == "GET":
        return render_template("admin/gameservers/refresh_accesskey.html", gameserver = GameServerObj)
    else:
        NewAccessKey = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(secrets.choice(range(60, 91))))
        req_response = gameserver_comm.perform_post(
            TargetGameserver = GameServerObj,
            Endpoint = "ResetAccessKeyAndRestart",
            JSONData = {
                "NewAccessKey": NewAccessKey
            },
            RequestTimeout = 30
        )

        if req_response.status_code != 200:
            flash("Failed to send request to game server", "danger")
            return redirect(f"/admin/gameservers/{serverid}")

        GameServerObj.accessKey = NewAccessKey
        db.session.commit()

        flash("Access key reset and game server restarted", "success")
        return redirect(f"/admin/gameservers/{serverid}")

@AdminRoute.route('/gameservers/create', methods=['GET'])
def gameservers_create():
    AdminPermissionRequired('GameServerManager')

    return render_template('admin/gameservers/create.html')

@AdminRoute.route('/gameservers/create', methods=['POST'])
def gameservers_create_post():
    AdminPermissionRequired('GameServerManager')
    if request.form['name'] == "" or request.form['serverip'] == "" or request.form['serverport'] == "" or request.form['accesskey'] == "":
        flash("Please fill in all fields", "danger")
        return redirect("/admin/gameservers/create")
    newServerID = str(uuid.uuid4())
    gameServer = GameServer(
        serverId = newServerID,
        serverName= request.form['name'],
        serverIP = request.form['serverip'],
        serverPort = request.form['serverport'],
        accessKey = request.form['accesskey'],
        allowThumbnailGen = True if 'isThumbnailer' in request.form else False,
        allowGameServerHost = True if 'isGameHoster' in request.form else False
    )
    db.session.add(gameServer)
    db.session.commit()

    return redirect("/admin/gameservers/" + newServerID)

@AdminRoute.route('/websitemessage', methods=['GET'])
def websitemessage():
    AdminPermissionRequired('UpdateWebsiteMessage')

    return render_template('admin/websitewidemsg.html', message = redis_controller.get("website_wide_message") or "")

@AdminRoute.route('/websitemessage', methods=['POST'])
def websitemessage_post():
    AdminPermissionRequired('UpdateWebsiteMessage')

    raw_message = request.form.get('message', '').strip()

    is_valid, error_msg = announcement.validate_announcement(raw_message)
    if not is_valid:
        flash(f"Invalid announcement: {error_msg}", "danger")
        return redirect("/admin/websitemessage")

    sanitized_message = announcement.sanitize_announcement(raw_message)

    AuthenticatedUser: User = auth.GetCurrentUser()
    announcement.log_announcement_to_discord(
        username=AuthenticatedUser.username,
        user_id=AuthenticatedUser.id,
        message=sanitized_message,
        action="updated"
    )

    redis_controller.set("website_wide_message", sanitized_message)
    flash("Announcement updated successfully", "success")
    return redirect("/admin/websitemessage")

@AdminRoute.route('/fflags', methods=['GET'])
def fflags():
    AdminPermissionRequired('ManageFFlags')

    return render_template('admin/fflagsettings/index.html', groups = FflagGroup.query.all())

@AdminRoute.route('/fflags/<int:groupid>', methods=['GET'])
def fflags_view(groupid : int):
    AdminPermissionRequired('ManageFFlags')

    FFlagGroupObj : FflagGroup = FflagGroup.query.filter_by(group_id=groupid).first()
    if FFlagGroupObj is None:
        return redirect("/admin/fflags")
    PageNumber = max( request.args.get('page', default=1, type=int), 1 )
    SearchQuery = request.args.get('search', default=None, type=str)
    GroupFlagsPagination = FflagValue.query.filter_by(group_id=groupid)

    if SearchQuery is not None:
        GroupFlagsPagination = GroupFlagsPagination.filter(FflagValue.name.ilike(f"%{SearchQuery}%"))

    GroupFlagsPagination = GroupFlagsPagination.paginate(
        page = PageNumber,
        per_page = 50,
        error_out = False
    )
    return render_template('admin/fflagsettings/view.html', group = FFlagGroupObj, FFlagsLookupResults = GroupFlagsPagination, flagcount = GroupFlagsPagination.total, search = SearchQuery)

@AdminRoute.route('/fflags/<int:groupid>/import', methods=['POST'])
def fflags_import(groupid: int):
    from app.routes.fflagssettings import ClearCache
    AdminPermissionRequired('ManageFFlags')

    group: FflagGroup = FflagGroup.query.filter_by(group_id=groupid).first()
    if group is None:
        return redirect("/admin/fflags")

    if 'file' not in request.files:
        flash("No file uploaded", "danger")
        return redirect(f"/admin/fflags/{str(groupid)}")
    
    file = request.files['file']
    
    if file.filename == '':
        flash("Filename empty", "danger")
        return redirect(f"/admin/fflags/{str(groupid)}")
    
    try:
        json.loads(file.read(2000000)) # no worker freezing (2mb limit)
    except:
        flash("Invalid JSON file", "danger")
        return redirect(f"/admin/fflags/{str(groupid)}")

    try:
        FflagValue.query.filter_by(group_id=groupid).delete()
        db.session.commit()

        file.seek(0)
        data = json.loads(file.read())
        for flag in data:
            FlagName = flag
            FlagValue = data[flag]
            
            FlagType = 1
            try:
                if isinstance(FlagValue, str):
                    if FlagValue.lower() == "true":
                        FlagValue = True
                    elif FlagValue.lower() == "false":
                        FlagValue = False
                    else:
                        raise Exception()
            except:
                try:
                    FlagValue = int(FlagValue)
                    FlagType = 2
                except:
                    FlagType = 3
            
            newFlag = FflagValue(
                group_id=groupid,
                name=FlagName,
                flag_type=FlagType,
                flag_value=base64.b64encode(str(FlagValue).encode('utf-8')).decode('utf-8')
            )
            db.session.add(newFlag)
        
        group.updated_at = datetime.utcnow()
        db.session.commit()
        ClearCache(groupid)
        flash("Flags imported", "success")
        return redirect(f"/admin/fflags/{str(groupid)}")
    
    except Exception as e:
        db.session.rollback()
        flash(f"Error importing flags: {str(e)}", "danger")
        return redirect(f"/admin/fflags/{str(groupid)}")

@AdminRoute.route('/fflags/create', methods=['GET', 'POST'])
def fflags_create():
    AdminPermissionRequired('ManageFFlags')

    if request.method == 'POST':
        name = request.form.get('name', '').strip()
        description = request.form.get('description', '').strip()
        enabled = request.form.get('enabled', 'false') == 'true'
        apikey = request.form.get('apikey', '').strip()
        gameserver_only = request.form.get('gameserver_only', 'false') == 'true'
        category = request.form.get('category', '2019+')  

        if not name:
            flash("Group name is required", "danger")
            return redirect("/admin/fflags/create")

        if not description:
            flash("Description is required", "danger")
            return redirect("/admin/fflags/create")

        if not apikey:
            flash("API Key is required", "danger")
            return redirect("/admin/fflags/create")

        if FflagGroup.query.filter_by(name=name).first() is not None:
            flash("A group with this name already exists", "danger")
            return redirect("/admin/fflags/create")

        try:
            if category == "2019+":
                if not name.startswith("application_"):
                    name = f"application_{name}"

            last_group = FflagGroup.query.order_by(FflagGroup.group_id.desc()).first()
            new_group_id = last_group.group_id + 1 if last_group else 1
            
            current_time = datetime.utcnow()
            new_group = FflagGroup(
                group_id=new_group_id,
                name=name,
                description=description,
                created_at=current_time,
                enabled=enabled,
                apikey=apikey
            )
            new_group.gameserver_only = gameserver_only
            new_group.updated_at = current_time
            
            db.session.add(new_group)
            db.session.commit()

            flash("FFlag group created successfully", "success")
            return redirect(f"/admin/fflags/{new_group.group_id}")
            
        except Exception as e:
            db.session.rollback()
            flash(f"Error creating FFlag group: {str(e)}", "danger")
            return redirect("/admin/fflags/create")

    return render_template('admin/fflagsettings/create.html')
  
  
@AdminRoute.route("/asset-copier", methods=['GET'])
def asset_copier():
    AdminPermissionRequired('CopyAssets')
    return render_template('admin/assetcopier.html')

@AdminRoute.route("/asset-copier", methods=['POST'])
def asset_copier_post():
    AdminPermissionRequired('CopyAssets')
    AssetURL = request.form.get('asseturl')
    if AssetURL is None:
        flash("No URL provided", "danger")
        return redirect("/admin/asset-copier")
    if AssetURL == "":
        flash("URL is empty", "danger")
        return redirect("/admin/asset-copier")
    AssetRegex = re.compile(r"https?://([a-z0-9-]+\.)*roblox\.com/catalog/(\d+)/") # makes sure the host is roblox, can be www.roblox.com or roblox.com or web.roblox.com etc etc
    AssetMatch = AssetRegex.search(request.form.get("asseturl"))
    if AssetMatch is None:
        flash("Invalid asset URL", "error")
        return redirect("/admin/asset-copier")
    AssetID = AssetMatch.group(2)
    # Check if asset exists
    if not AssetID.isdigit(): abort(400) # prevent potential dos
    AssetObj = Asset.query.filter_by(roblox_asset_id=int(AssetID)).first()
    if AssetObj is not None:
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    
    AuthenticatedUser = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
    AssetMigrationCooldown = redis_controller.get(f"asset_migration_cooldown_{str(AuthenticatedUser.id)}")
    if AssetMigrationCooldown is not None and AuthenticatedUser.id != 1:
        flash(f"You are copying assets too fast!", "danger")
        return redirect("/admin/asset-copier")
    redis_controller.set(f"asset_migration_cooldown_{str(AuthenticatedUser.id)}", "1", 20)

    # Migrate asset
    NewAsset : Asset = migrateAsset(int(AssetID), forceMigration=False, allowedTypes=[2, 8, 11, 12, 17, 18, 19, 41, 42, 43, 44, 45, 46, 47], creatorId=1, keepRobloxId=False, migrateInfo=True)
    if NewAsset is None:
        flash("Failed to migrate asset", "danger")
        return redirect("/admin/asset-copier")
    TakeThumbnail(NewAsset.id)
    return redirect(f"/admin/manage-assets/{str(NewAsset.id)}")

def IsItemInReleasePool( AssetId : int ) -> bool:
    return redis_controller.lrange("ItemReleasePool:Items", 0, -1).count(str(AssetId)) > 0
def GetNextItemDropDateTime() -> datetime:
    LastItemDropTimestamp : str | None = redis_controller.get("ItemReleasePool:LastDropTimestamp")
    if LastItemDropTimestamp is None:
        LastItemDropTimestamp = round(time.time())
    else:
        try:
            LastItemDropTimestamp = int(LastItemDropTimestamp)
        except Exception as e:
            logging.warn(f"Admin > GetNextItemDropDateTime: Exception raised while trying to convert to integer, {str(e)}")
            LastItemDropTimestamp = round(time.time())
    RandomSeed = f"{LastItemDropTimestamp}-{config.FLASK_SESSION_KEY}"
    random.seed( RandomSeed )
    NextDropDatetime = datetime.utcfromtimestamp( LastItemDropTimestamp + random.randint( 60 * 60 * 6, 60 * 60 * 20 ) )
    return NextDropDatetime

def InsertItemIntoItemReleasePool( AssetObj : Asset, Name : str, Description : str, RobuxPrice : int = 0, TicketsPrice : int = 0, IsLimited : bool = False, IsLimitedUnique : bool = False, SerialCount : int | None = None, OffsaleAfter : timedelta | None = None ):
    if IsItemInReleasePool( AssetObj.id ):
        raise Exception("InsertItemIntoItemReleasePool: Attempted to insert item which is already inside item release pool")
    if RobuxPrice < 0 or TicketsPrice < 0:
        raise Exception("InsertItemIntoItemReleasePool: RobuxPrice and TicketsPrice cannot be a negative integer")
    if IsLimitedUnique:
        IsLimited = True
    
    ItemReleaseMetaData = json.dumps({
        "Name" : Name,
        "Description" : Description,
        "RobuxPrice" : RobuxPrice,
        "TicketsPrice" : TicketsPrice,
        "IsLimited" : IsLimited,
        "IsLimitedUnique" : IsLimitedUnique,
        "SerialCount": SerialCount,
        "OffsaleAfter" : None if OffsaleAfter is None else OffsaleAfter.total_seconds()
    })

    redis_controller.set(f"ItemReleasePool:Item_Metadata:{AssetObj.id}", ItemReleaseMetaData)
    redis_controller.rpush("ItemReleasePool:Items", str(AssetObj.id))
    logging.info(f"Inserted Item {AssetObj.id} into Item Release Pool")

    AssetObj.name = "Asset"
    AssetObj.description = ""
    AssetObj.price_robux = 0
    AssetObj.price_tix = 0
    AssetObj.is_limited = False
    AssetObj.is_limited_unique = False
    AssetObj.moderation_status = 2 # Hides the asset from people scraping every asset

    db.session.commit()

@AdminRoute.route("/manage-assets/<int:assetid>", methods=['GET'])
@AdminRoute.route("/manage-assets", methods=['GET'])
def manage_assets(assetid = None):
    AdminPermissionRequired('ManageAsset')
    if assetid is not None:
        AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
        if AssetObj is None:
            flash("Asset does not exist", "danger")
            return redirect("/admin/manage-assets")
        if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
            flash("Asset is not a offical asset", "danger")
            return redirect("/admin/manage-assets")
        if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:#[1,2,3,4,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
            flash("Asset type is not supported", "danger")
            return redirect("/admin/manage-assets")
        CanManageItemReleasePool = HasAdminPermission("ManageItemReleases")
        IsItemEligibleForItemReleasePool = AssetObj.is_for_sale is False and AssetObj.is_limited is False and AssetObj.sale_count == 0
        return render_template('admin/manageassets.html', asset = AssetObj, CanManageItemReleasePool = CanManageItemReleasePool, IsItemEligibleForItemReleasePool = IsItemEligibleForItemReleasePool)
    return render_template('admin/manageassets.html')

@AdminRoute.route("/manage-assets/<int:assetid>/insert-item-pool", methods=['GET'])
def insert_item_into_pool( assetid : int ):
    AdminPermissionRequired('ManageAsset')
    AdminPermissionRequired('ManageItemReleases')

    AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/manage-assets")
    if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
        flash("Asset is not a offical asset", "danger")
        return redirect("/admin/manage-assets")
    if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:#[1,2,3,4,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
        flash("Asset type is not supported", "danger")
        return redirect("/admin/manage-assets")
    
    if IsItemInReleasePool(AssetObj.id):
        flash("Item is already inside the item release pool", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    IsItemEligibleForItemReleasePool = AssetObj.is_for_sale is False and AssetObj.is_limited is False and AssetObj.sale_count == 0
    if not IsItemEligibleForItemReleasePool:
        flash("Item is not eligible for item release pool", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    
    return render_template('admin/insertitemrelasepool.html', asset = AssetObj)

@AdminRoute.route("/manage-assets/<int:assetid>/insert-item-pool", methods=['POST'])
def insert_item_into_pool_post( assetid : int ):
    AdminPermissionRequired('ManageAsset')
    AdminPermissionRequired('ManageItemReleases')

    AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/manage-assets")
    if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
        flash("Asset is not a offical asset", "danger")
        return redirect("/admin/manage-assets")
    if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:#[1,2,3,4,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
        flash("Asset type is not supported", "danger")
        return redirect("/admin/manage-assets")
    
    if IsItemInReleasePool(AssetObj.id):
        flash("Item is already inside the item release pool", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    IsItemEligibleForItemReleasePool = AssetObj.is_for_sale is False and AssetObj.is_limited is False and AssetObj.sale_count == 0
    if not IsItemEligibleForItemReleasePool:
        flash("Item is not eligible for item release pool", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")

    AssetName = request.form.get('assetname', default=AssetObj.name, type=str)
    AssetDescription = request.form.get('assetdescription', default=AssetObj.description, type=str)
    AssetPriceRobux = request.form.get('assetpricerobux', default = None, type=int)
    AssetPriceTickets = request.form.get('assetpricetix', default = None, type=int)
    AssetSerialAmount = request.form.get('assetserialamount', default = None, type=int)
    isLimited = request.form.get('isLimited') == "on"
    isLimitedUnique = request.form.get('isLimitedUnique') == "on"

    if AssetName is None or AssetName == "":
        flash("Asset name is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
    if AssetDescription is None:
        flash("Asset description is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")

    if isLimitedUnique:
        isLimited = True
    if not HasAdminPermission("ModifyLimitedAssets") and isLimited:
        flash("You do not have permission to create Limiteds", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
    if AssetSerialAmount < 0:
        flash("Asset serial amount cannot be negative", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
    if AssetPriceRobux < 0 or AssetPriceRobux > 1000000:
        flash("Asset Robux price must be in range ( 0 - 1000000)", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
    if AssetPriceTickets < 0 or AssetPriceTickets > 10000000:
        flash("Asset Tix price must be in range ( 0 - 10000000)", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
    if AssetSerialAmount > 0 and not isLimitedUnique:
        flash("Asset serial cannot be greater than 0 if not a limited unique", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")

    AssetOffsaleHours = request.form.get('offsale-at-hours', default=None, type=int)
    AssetOffsaleMinutes = request.form.get('offsale-at-minutes', default=None, type=int)
    AssetOffsaleSeconds = request.form.get('offsale-at-seconds', default=None, type=int)
    if AssetOffsaleHours is not None or AssetOffsaleMinutes is not None or AssetOffsaleSeconds is not None:
        AssetOffsaleHours = AssetOffsaleHours or 0
        AssetOffsaleMinutes = AssetOffsaleMinutes or 0
        AssetOffsaleSeconds = AssetOffsaleSeconds or 0

        OffsaleDelta = timedelta(
            hours = AssetOffsaleHours,
            minutes = AssetOffsaleMinutes,
            seconds = AssetOffsaleSeconds
        )
        if timedelta(minutes = 5) > OffsaleDelta:
            flash("Offsale Time cannot be lesser than 5 minutes", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")
    else:
        OffsaleDelta = None

    if OffsaleDelta and isLimited:
        flash("Limited unique assets cannot have a offsale date", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}/insert-item-pool")

    InsertItemIntoItemReleasePool(
        AssetObj = AssetObj,
        Name = AssetName,
        Description = AssetDescription,
        RobuxPrice = AssetPriceRobux,
        TicketsPrice = AssetPriceTickets,
        IsLimited = isLimited,
        IsLimitedUnique = isLimitedUnique,
        SerialCount = AssetSerialAmount
    )

    flash("Successfully inserted item into item release pool", "success")
    return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")

@AdminRoute.route("/item-release-pool", methods=["GET"])
def item_release_pool_view():
    AdminPermissionRequired('ManageItemReleases')
    PlannedItems = []
    AllAssetIds = redis_controller.lrange("ItemReleasePool:Items", 0, -1)
    for AssetId in AllAssetIds:
        AssetMetadata = redis_controller.get(f"ItemReleasePool:Item_Metadata:{AssetId}")
        if AssetMetadata is None:
            logging.warn(f"item_release_pool_view failed to get asset metadata for {AssetId}, removing it from list")
            redis_controller.lrem("ItemReleasePool:Items", 0, AssetId)
            return
        try:
            AssetMetadata = json.loads(AssetMetadata)
        except Exception as e:
            logging.warn(f"item_release_pool_view failed to parse json from item metadata {AssetId}, {str(e)}")
            redis_controller.lrem("ItemReleasePool:Items", 0, AssetId)
            return
        
        PlannedItems.append({
            "id": int(AssetId),
            "name": AssetMetadata["Name"],
            "description": AssetMetadata["Description"],
            "robux_price": AssetMetadata["RobuxPrice"],
            "tickets_price": AssetMetadata["TicketsPrice"],
            "is_limited": AssetMetadata["IsLimited"],
            "is_limited_unique": AssetMetadata["IsLimitedUnique"],
            "serial_count": AssetMetadata["SerialCount"],
            "offsale_after": AssetMetadata["OffsaleAfter"]
        })
    
    if HasAdminPermission("ViewItemReleasePoolDrop"):
        NextItemDropDatetime = f"{GetNextItemDropDateTime()} UTC"
    else:
        NextItemDropDatetime = "You do not have permission to view the next Drop Time"

    return render_template("admin/itemreleasepool.html", PlannedItems=PlannedItems, NextItemDropDatetime=NextItemDropDatetime)


@AdminRoute.route("/manage-assets", methods=['POST'])
def manage_assets_post():
    AdminPermissionRequired('ManageAsset')
    AssetID = request.form.get('assetid')
    if AssetID is None:
        flash("No asset ID provided", "danger")
        return redirect("/admin/manage-assets")
    if AssetID == "":
        flash("Asset ID is empty", "danger")
        return redirect("/admin/manage-assets")
    AssetObj : Asset = Asset.query.filter_by(id=int(AssetID)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/manage-assets")
    if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
        flash("Asset is not a offical asset", "danger")
        return redirect("/admin/manage-assets")
    if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:
        flash("Asset type is not supported", "danger")
        return redirect("/admin/manage-assets")
    return redirect(f"/admin/manage-assets/{str(AssetID)}")

from app.extensions import scheduler
def SetAssetOffsaleJob( assetId : int ):
    with scheduler.app.app_context():
        AssetObj : Asset = Asset.query.filter_by(id=assetId).first()
        if AssetObj is None:
            return
        if AssetObj.offsale_at is None:
            return
        if AssetObj.offsale_at > datetime.utcnow() + timedelta(minutes=5):
            logging.warning(f"Asset {str(AssetObj.id)} has a offsale date in the future more than 5 minutes diff, skipping")
            return
        AssetObj.is_for_sale = False
        AssetObj.offsale_at = None
        db.session.commit()
        logging.info(f"Asset {str(AssetObj.id)} has been set offsale at {str(datetime.utcnow())}")
        redis_controller.delete(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}")

@AdminRoute.route("/manage-assets/<int:assetid>", methods=['POST'])
def manage_assets_post_update(assetid):
    AdminPermissionRequired('ManageAsset')
    AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/manage-assets")
    if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0: # Only allows assets under ROBLOX and UGC accounts
        flash("Asset is not a offical asset", "danger")
        return redirect("/admin/manage-assets")
    if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:
        flash("Asset type is not supported", "danger")
        return redirect("/admin/manage-assets")
    if AssetObj.is_limited or AssetObj.is_limited_unique:
        if not HasAdminPermission("ModifyLimitedAssets"):
            flash("You do not have permission to modify limited assets", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if IsItemInReleasePool(AssetObj.id):
        flash("You cannot edit an item which is in the item release pool", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    # Validate form
    AssetName = request.form.get('assetname')
    AssetDescription = request.form.get('assetdescription')
    AssetPriceRobux = request.form.get('assetpricerobux')
    AssetPriceTickets = request.form.get('assetpricetix')
    AssetSerialAmount = request.form.get('assetserialamount')

    AssetOffsaleAt = request.form.get('offsale-at', None) # Year-Month-DayTHour:Minute
    OffsaleTimezone = request.form.get('offsale-timezone', 0, int) # 0, 1, 2, ... or -1, -2, ...
    if AssetOffsaleAt is not None and AssetOffsaleAt != "":
        try:
            AssetOffsaleAt = datetime.strptime(AssetOffsaleAt, "%Y-%m-%dT%H:%M")
            # We need to convert the time to UTC
            AssetOffsaleAt = AssetOffsaleAt.replace(tzinfo=timezone(timedelta(hours=OffsaleTimezone)))
            AssetOffsaleAt = AssetOffsaleAt.astimezone(timezone.utc)
            AssetOffsaleAt = datetime(
                AssetOffsaleAt.year,
                AssetOffsaleAt.month,
                AssetOffsaleAt.day,
                AssetOffsaleAt.hour,
                AssetOffsaleAt.minute,
                AssetOffsaleAt.second,
                AssetOffsaleAt.microsecond
            )
        except:
            flash("Invalid offsale date", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
        if datetime.utcnow() > AssetOffsaleAt:
            flash("Offsale date cannot be in the past", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    else:
        AssetOffsaleAt = None

    isForSale = request.form.get('isForsale') == "on"
    isLimited = request.form.get('isLimited') == "on"
    isLimitedUnique = request.form.get('isLimitedUnique') == "on"

    if AssetName is None or AssetName == "":
        flash("Asset name is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetDescription is None:
        flash("Asset description is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetPriceRobux is None or AssetPriceRobux == "":
        flash("Asset price is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    else:
        try:
            AssetPriceRobux = int(AssetPriceRobux)
        except:
            flash("Asset price is not a number", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetPriceTickets is None or AssetPriceTickets == "":
        flash("Asset price is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    else:
        try:
            AssetPriceTickets = int(AssetPriceTickets)
        except:
            flash("Asset price is not a number", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetSerialAmount is None or AssetSerialAmount == "":
        flash("Asset serial amount is empty", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    else:
        try:
            AssetSerialAmount = int(AssetSerialAmount)
        except:
            flash("Asset serial amount is not a number", "danger")
            return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetSerialAmount < 0:
        flash("Asset serial amount cannot be negative", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetPriceRobux < 0 or AssetPriceRobux > 1000000:
        flash("Asset Robux price must be in range ( 0 - 1000000)", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetPriceTickets < 0 or AssetPriceTickets > 10000000:
        flash("Asset Tix price must be in range ( 0 - 10000000)", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetSerialAmount > 0 and not isLimitedUnique:
        flash("Asset serial cannot be greater than 0 if not a limited unique", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")

    AuthenticatedUser : User = auth.GetCurrentUser()
    # Check for cooldown
    AdministratorCooldown = redis_controller.get(f"Asset_AdministratorCooldown:{str(AuthenticatedUser.id)}")
    if AdministratorCooldown is not None and AuthenticatedUser.id != 1:
        flash("You are updating assets too quickly!", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    redis_controller.set(f"Asset_AdministratorCooldown:{str(AuthenticatedUser.id)}", "1", ex=20)

    if isLimitedUnique:
        isLimited = True

    if isLimited:
        if not HasAdminPermission("ModifyLimitedAssets"):
            flash("You do not have permission to modify limited assets", "danger")
            return redirect("/admin/manage-assets")

    if isLimitedUnique and AssetOffsaleAt is not None:
        flash("Limited unique assets cannot have a offsale date", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if not isForSale and AssetOffsaleAt is not None:
        flash("Asset cannot have a offsale date if not for sale", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    if AssetOffsaleAt is not None and AssetOffsaleAt < datetime.utcnow() + timedelta( minutes = 5 ):
        flash("Offsale time cannot be shorter than 5 minutes", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")

    # Update asset
    AssetObj.name = AssetName
    AssetObj.description = AssetDescription
    AssetObj.price_robux = AssetPriceRobux
    AssetObj.price_tix = AssetPriceTickets
    AssetObj.serial_count = AssetSerialAmount
    AssetObj.is_for_sale = isForSale
    AssetObj.is_limited = isLimited
    AssetObj.is_limited_unique = isLimitedUnique
    AssetObj.updated_at = datetime.utcnow()
    
    if AssetOffsaleAt is not None and AssetObj.offsale_at != AssetOffsaleAt:
        AssetObj.offsale_at = AssetOffsaleAt
        if redis_controller.exists(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}"):
            try:
                scheduler.remove_job(redis_controller.get(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}"))
            except:
                logging.warning(f"Failed to remove job {redis_controller.get(f'APSchedulerTaskJobUUID:{str(AssetObj.id)}')}")

        APSchedulerTaskJobUUID = str(uuid.uuid4())
        scheduler.add_job(id=APSchedulerTaskJobUUID, func=SetAssetOffsaleJob, trigger='date', run_date=AssetOffsaleAt, args=[AssetObj.id])
        redis_controller.set(f"APSchedulerTaskJobUUID:{str(AssetObj.id)}", APSchedulerTaskJobUUID)
        logging.info(f"Asset {str(AssetObj.id)} has been set to go offsale at {str(AssetOffsaleAt)}, job UUID: {APSchedulerTaskJobUUID}")
    if not isForSale:
        AssetObj.offsale_at = None

    db.session.commit()
    flash("Asset updated!", "success")
    return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")

@AdminRoute.route("/manage-assets/<int:assetid>/rerender", methods=['POST'])
def ReRenderAsset(assetid):
    AdminPermissionRequired('ManageAsset')
    AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/manage-assets")
    if (AssetObj.creator_id != 1 and AssetObj.creator_id != 2) and AssetObj.creator_type != 0:
        flash("Asset is not a offical asset", "danger")
        return redirect("/admin/manage-assets")
    # Check for cooldown
    AuthenticatedUser : User = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
    AdministratorCooldown = redis_controller.get(f"AssetRerender_AdministratorCooldown:{str(AuthenticatedUser.id)}")
    if AdministratorCooldown is not None and AuthenticatedUser.id != 1:
        flash("You are rerendering assets too quickly!", "danger")
        return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")
    redis_controller.set(f"AssetRerender_AdministratorCooldown:{str(AuthenticatedUser.id)}", "1", ex=5)

    TakeThumbnail(AssetObj.id, bypassCooldown=True, bypassCache=True)
    flash("Asset queued for rerendering", "success")
    return redirect(f"/admin/manage-assets/{str(AssetObj.id)}")

@AdminRoute.route("/pending-assets", methods=['GET'])
def PendingAssets():
    AdminPermissionRequired('AssetModeration')
    
    PendingAssets = []
    # Only get assets that have their moderation_status set to 1
    AssetsPendingList : list[Asset] = Asset.query.filter_by(moderation_status=1).order_by(Asset.created_at.desc()).all()
    for AssetObj in AssetsPendingList:
        if AssetObj.asset_type not in [AssetType.Image, AssetType.Audio, AssetType.Animation]:
            continue
        LatestAssetVersion : AssetVersion = assetversion.GetLatestAssetVersion(AssetObj)
        if LatestAssetVersion is None:
            continue
        ParentAsset : Asset = None
        if AssetObj.asset_type == AssetType.Image:
            # Try to get the parent asset
            ParentAssetLink : AssetModerationLink = AssetModerationLink.query.filter_by(ChildAssetId=AssetObj.id).first()
            if ParentAssetLink is not None:
                ParentAsset = Asset.query.filter_by(id=ParentAssetLink.ParentAssetId).first()
        PendingAssets.append({
            "Asset": AssetObj,
            "AssetVersion": LatestAssetVersion,
            "ParentAsset": ParentAsset
        })
        if len(PendingAssets) >= 15:
            break
    PlaceIconPendingList : list[PlaceIcon] = PlaceIcon.query.filter_by(moderation_status=1).order_by(PlaceIcon.updated_at.desc()).all()
    for PlaceIconObj in PlaceIconPendingList:
        AssetObj : Asset = Asset.query.filter_by(id=PlaceIconObj.placeid).first()
        if AssetObj is None:
            continue
        PendingAssets.append({
            "Asset": AssetObj,
            "AssetVersion": PlaceIconObj,
            "ParentAsset": None,
            "PlaceIcon": True,
            "AssetThumbnail": False
        })
        if len(PendingAssets) >= 15:
            break
    
    AssetThumbnailPendingList : list[AssetThumbnail] = AssetThumbnail.query.filter_by(moderation_status=1).order_by(AssetThumbnail.updated_at.desc()).all()
    for AssetThumbnailObj in AssetThumbnailPendingList:
        AssetObj : Asset = Asset.query.filter_by(id=AssetThumbnailObj.asset_id).first()
        if AssetObj is None:
            continue
        PendingAssets.append({
            "Asset": AssetObj,
            "AssetVersion": AssetThumbnailObj,
            "ParentAsset": None,
            "PlaceIcon": False,
            "AssetThumbnail": True
        })
    
    GroupIconPendingList : list[GroupIcon] = GroupIcon.query.filter_by(moderation_status=1).order_by(GroupIcon.created_at.desc()).all()
    for GroupIconObj in GroupIconPendingList:
        GroupObj : Group = GroupIconObj.group
        if GroupObj is None:
            continue
        PendingAssets.append({
            "Group": GroupObj,
            "Creator": GroupIconObj.creator,
            "Icon": GroupIconObj,
            "ParentAsset": None,
            "PlaceIcon": False,
            "AssetThumbnail": False
        })

    return render_template("admin/assetmoderation.html", PendingAssets=PendingAssets)

def LogModerationAction(
    Actor : User,
    isApproved : bool = True,
    relatedAssets : list = []
):
    """
        relatedAssets = [
            {
                "mainId": 1,
                "relatedId": 2,
                "type": "Image",
                "view_name": "Awesome Shirt",
                "source": "https://NEXIUM.economy-simulator.org/",
                "page": "https://www.nexium.fit/catalog/1/",
                "creator": User | Group
            }
        ]
    """
    fields = []
    for asset in relatedAssets:
        fields.append({
            "name": asset["view_name"],
            "value": f"Type: **{asset['type']}** - { 'AssetId' if asset['type'] != 'GroupIcon' else 'GroupId' }: **{asset['mainId']}** - Creator: [{ asset['creator'].username if isinstance(asset['creator'], User) else asset['creator'].name }](https://www.nexium.fit/{ 'users/'+str(asset['creator'].id)+'/profile' if isinstance(asset['creator'], User) else 'groups/'+str(asset['creator'].id)+'/' }){ ' - Related AssetId: **' + str(asset['relatedId']) +'**' if asset['relatedId'] else ''} - [View]({asset['page']}){ ' - [Source](' + asset['source'] +')' if asset['source'] else ''}",
            "inline": False
        })
    
    embed = {
        "type": "rich",
        "title": f"{'Approved' if isApproved else 'Denied'} {str(len(relatedAssets))} Asset{'s' if len(relatedAssets) > 1 else ''}",
        "description": "",
        "color": 0x26ff00 if isApproved else 0xef0000,
        "fields": fields,
        "author": {
            "name": Actor.username,
            "icon_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
        },
        "footer": {
            "text": f"NEXIUM - Moderation Logs"
        },
        "timestamp": datetime.utcnow().isoformat()
    }
    def thread_func():
        try:
            requests.post(
                url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
                json = {
                    "username": "NEXIUM - Moderation Logs",
                    "embeds": [embed],
                    "avatar_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
                },
                timeout = 15 # without timeout the thread will hang if request hangs
            )
        except Exception as e:
            logging.warn(f"Admin > LogModerationAction: Exception raised when sending webhook - {str(e)}")
    threading.Thread(target=thread_func).start()

@AdminRoute.route("/pending-assets/<content_hash>/approve-group-icon", methods=['POST'])
@csrf.exempt
def ApprovePendingGroupIcon( content_hash ):
    AdminPermissionRequired('AssetModeration')

    GroupIconObj : list[GroupIcon] = GroupIcon.query.filter_by(content_hash=content_hash).all()
    if len(GroupIconObj) == 0:
        return redirect("/admin/pending-assets")
    for iconObj in GroupIconObj:
        if iconObj.moderation_status != 1:
            continue
        LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=[{
            "mainId": iconObj.group.id,
            "relatedId": None,
            "type": "GroupIcon",
            "view_name": iconObj.group.name,
            "source": f"{config.CDN_URL}/{iconObj.content_hash}",
            "page": f"https://www.nexium.fit/groups/{str(iconObj.group.id)}/",
            "creator": iconObj.creator
        }])
        iconObj.moderation_status = 0
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<content_hash>/deny-group-icon", methods=['POST'])
@csrf.exempt
def DenyPendingGroupIcon( content_hash ):
    AdminPermissionRequired('AssetModeration')

    GroupIconObj : list[GroupIcon] = GroupIcon.query.filter_by(content_hash=content_hash).all()
    if len(GroupIconObj) == 0:
        return redirect("/admin/pending-assets")
    for iconObj in GroupIconObj:
        if iconObj.moderation_status != 1:
            continue
        LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=[{
            "mainId": iconObj.group.id,
            "relatedId": None,
            "type": "GroupIcon",
            "view_name": iconObj.group.name,
            "source": f"{config.CDN_URL}/{iconObj.content_hash}",
            "page": f"https://www.nexium.fit/groups/{str(iconObj.group.id)}/",
            "creator": iconObj.creator
        }])
        iconObj.moderation_status = 2
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<int:assetid>/approve", methods=['POST'])
@csrf.exempt
def ApprovePendingAsset(assetid):
    AdminPermissionRequired('AssetModeration')

    AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/pending-assets")
    if AssetObj.moderation_status != 1:
        flash("Asset is not pending", "danger")
        return redirect("/admin/pending-assets")
    if AssetObj.asset_type not in [AssetType.Image, AssetType.Audio, AssetType.Place, AssetType.Animation]:
        flash("Asset type is not supported", "danger")
        return redirect("/admin/pending-assets")
    RelatedAssets = []
    AssetObj.moderation_status = 0
    RelatedAssets.append({
        "mainId": AssetObj.id,
        "relatedId": None,
        "type": "Asset",
        "view_name": AssetObj.name,
        "source": None,
        "page": f"https://www.nexium.fit/catalog/{str(AssetObj.id)}/",
        "creator": GetCreatorOfAsset(AssetObj)
    })
    # Get the AssetThumbnail
    AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).first()
    if AssetThumbnailObj is not None:
        if AssetThumbnailObj.moderation_status == 1:
            AssetThumbnailObj.moderation_status = 0
            RelatedAssets.append({
                "mainId": AssetObj.id,
                "relatedId": None,
                "type": "AssetThumbnail",
                "view_name": AssetObj.name,
                "source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
                "page": f"https://www.nexium.fit/catalog/{str(AssetObj.id)}/",
                "creator": GetCreatorOfAsset(AssetObj)
            })

    # Get the parent asset
    ParentAssetLink : AssetModerationLink = AssetModerationLink.query.filter_by(ChildAssetId=AssetObj.id).first()
    if ParentAssetLink is not None:
        ParentAsset : Asset = Asset.query.filter_by(id=ParentAssetLink.ParentAssetId).first()
        if ParentAsset is not None:
            if ParentAsset.moderation_status == 1:
                ParentAsset.moderation_status = 0
                RelatedAssets.append({
                    "mainId": ParentAsset.id,
                    "relatedId": AssetObj.id,
                    "type": AssetObj.asset_type.name,
                    "view_name": ParentAsset.name,
                    "source": None,
                    "page": f"https://www.nexium.fit/catalog/{str(ParentAsset.id)}/",
                    "creator": GetCreatorOfAsset(AssetObj)
                })
            # Get the AssetThumbnail
            ParentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=ParentAsset.id).first()
            if ParentAssetThumbnail is not None:
                if ParentAssetThumbnail.moderation_status == 1:
                    ParentAssetThumbnail.moderation_status = 0
                    RelatedAssets.append({
                        "mainId": ParentAsset.id,
                        "relatedId": AssetObj.id,
                        "type": "AssetThumbnail",
                        "view_name": ParentAsset.name,
                        "source": f"{config.CDN_URL}/{ParentAssetThumbnail.content_hash}",
                        "page": f"https://www.nexium.fit/catalog/{str(ParentAsset.id)}/",
                        "creator": GetCreatorOfAsset(AssetObj)
                    })
    LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=RelatedAssets)
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<int:assetid>/decline", methods=['POST'])
@csrf.exempt
def DeclinePendingAsset(assetid):
    AdminPermissionRequired('AssetModeration')

    AssetObj : Asset = Asset.query.filter_by(id=int(assetid)).first()
    if AssetObj is None:
        flash("Asset does not exist", "danger")
        return redirect("/admin/pending-assets")
    if AssetObj.moderation_status != 1:
        flash("Asset is not pending", "danger")
        return redirect("/admin/pending-assets")
    if AssetObj.asset_type not in [AssetType.Image, AssetType.Audio, AssetType.Place, AssetType.Animation]:
        flash("Asset type is not supported", "danger")
        return redirect("/admin/pending-assets")
    RelatedAssets = []
    AssetObj.moderation_status = 2
    RelatedAssets.append({
        "mainId": AssetObj.id,
        "relatedId": None,
        "type": "Asset",
        "view_name": AssetObj.name,
        "source": None,
        "page": f"https://www.nexium.fit/catalog/{str(AssetObj.id)}/",
        "creator": GetCreatorOfAsset(AssetObj)
    })
    # Get the AssetThumbnail
    AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).first()
    if AssetThumbnailObj is not None:
        if AssetThumbnailObj.moderation_status == 1:
            AssetThumbnailObj.moderation_status = 2
            RelatedAssets.append({
                "mainId": AssetObj.id,
                "relatedId": None,
                "type": "AssetThumbnail",
                "view_name": AssetObj.name,
                "source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
                "page": f"https://www.nexium.fit/catalog/{str(AssetObj.id)}/",
                "creator": GetCreatorOfAsset(AssetObj)
            })

    # Get the parent asset
    ParentAssetLink : AssetModerationLink = AssetModerationLink.query.filter_by(ChildAssetId=AssetObj.id).first()
    if ParentAssetLink is not None:
        ParentAsset : Asset = Asset.query.filter_by(id=ParentAssetLink.ParentAssetId).first()
        if ParentAsset is not None:
            if ParentAsset.moderation_status == 1:
                ParentAsset.moderation_status = 2
                RelatedAssets.append({
                    "mainId": ParentAsset.id,
                    "relatedId": AssetObj.id,
                    "type": AssetObj.asset_type.name,
                    "view_name": ParentAsset.name,
                    "source": None,
                    "page": f"https://www.nexium.fit/catalog/{str(ParentAsset.id)}/",
                    "creator": GetCreatorOfAsset(AssetObj)
                })
            # Get the AssetThumbnail
            ParentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=ParentAsset.id).first()
            if ParentAssetThumbnail is not None:
                ParentAssetThumbnail.moderation_status = 2
                RelatedAssets.append({
                    "mainId": ParentAsset.id,
                    "relatedId": AssetObj.id,
                    "type": "AssetThumbnail",
                    "view_name": ParentAsset.name,
                    "source": f"{config.CDN_URL}/{ParentAssetThumbnail.content_hash}",
                    "page": f"https://www.nexium.fit/catalog/{str(ParentAsset.id)}/",
                    "creator": GetCreatorOfAsset(AssetObj)
                })
    LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=RelatedAssets)
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<int:assetid>/approve-icon", methods=['POST'])
@csrf.exempt
def ApprovePendingIcon(assetid):
    AdminPermissionRequired('AssetModeration')

    PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=int(assetid)).first()
    if PlaceIconObj is None:
        flash("Icon does not exist", "danger")
        return redirect("/admin/pending-assets")
    if PlaceIconObj.moderation_status != 1:
        flash("Icon is not pending", "danger")
        return redirect("/admin/pending-assets")
    PlaceAssetObj : Asset = Asset.query.filter_by(id=PlaceIconObj.placeid).first()
    if PlaceAssetObj is None:
        flash("Icon does not exist", "danger")
        return redirect("/admin/pending-assets")
    if PlaceAssetObj.moderation_status == 1:
        PlaceAssetObj.moderation_status = 0
    PlaceIconObj.moderation_status = 0
    LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=[{
        "mainId": PlaceAssetObj.id,
        "relatedId": None,
        "type": "PlaceIcon",
        "view_name": PlaceAssetObj.name,
        "source": f"{config.CDN_URL}/{PlaceIconObj.contenthash}",
        "page": f"https://www.nexium.fit/games/{str(PlaceAssetObj.id)}/",
        "creator": GetCreatorOfAsset(PlaceAssetObj)
    }])
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<int:assetid>/decline-icon", methods=['POST'])
@csrf.exempt
def DeclinePendingIcon(assetid):
    AdminPermissionRequired('AssetModeration')

    PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=int(assetid)).first()
    if PlaceIconObj is None:
        flash("Icon does not exist", "danger")
        return redirect("/admin/pending-assets")
    if PlaceIconObj.moderation_status != 1:
        flash("Icon is not pending", "danger")
        return redirect("/admin/pending-assets")
    PlaceIconObj.moderation_status = 2
    LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=[{
        "mainId": PlaceIconObj.placeid,
        "relatedId": None,
        "type": "PlaceIcon",
        "view_name": PlaceIconObj.asset.name,
        "source": f"{config.CDN_URL}/{PlaceIconObj.contenthash}",
        "page": f"https://www.nexium.fit/games/{str(PlaceIconObj.placeid)}/",
        "creator": GetCreatorOfAsset(PlaceIconObj.asset)
    }])
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<int:thumbnailid>/approve-thumbnail", methods=['POST'])
@csrf.exempt
def ApprovePendingThumbnail(thumbnailid : int):
    AdminPermissionRequired('AssetModeration')

    AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(id=int(thumbnailid)).first()
    if AssetThumbnailObj is None:
        flash("Thumbnail does not exist", "danger")
        return redirect("/admin/pending-assets")
    if AssetThumbnailObj.moderation_status != 1:
        flash("Thumbnail is not pending", "danger")
        return redirect("/admin/pending-assets")
    AssetThumbnailObj.moderation_status = 0
    LogModerationAction(auth.GetCurrentUser(), isApproved=True, relatedAssets=[{
        "mainId": AssetThumbnailObj.asset_id,
        "relatedId": None,
        "type": "AssetThumbnail",
        "view_name": AssetThumbnailObj.asset.name,
        "source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
        "page": f"https://www.nexium.fit/catalog/{str(AssetThumbnailObj.asset_id)}/",
        "creator": GetCreatorOfAsset(AssetThumbnailObj.asset)
    }])
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/pending-assets/<int:thumbnailid>/decline-thumbnail", methods=['POST'])
@csrf.exempt
def DeclinePendingThumbnail(thumbnailid : int):
    AdminPermissionRequired('AssetModeration')

    AssetThumbnailObj : AssetThumbnail = AssetThumbnail.query.filter_by(id=int(thumbnailid)).first()
    if AssetThumbnailObj is None:
        flash("Thumbnail does not exist", "danger")
        return redirect("/admin/pending-assets")
    if AssetThumbnailObj.moderation_status != 1:
        flash("Thumbnail is not pending", "danger")
        return redirect("/admin/pending-assets")
    AssetThumbnailObj.moderation_status = 2
    LogModerationAction(auth.GetCurrentUser(), isApproved=False, relatedAssets=[{
        "mainId": AssetThumbnailObj.asset_id,
        "relatedId": None,
        "type": "AssetThumbnail",
        "view_name": AssetThumbnailObj.asset.name,
        "source": f"{config.CDN_URL}/{AssetThumbnailObj.content_hash}",
        "page": f"https://www.nexium.fit/catalog/{str(AssetThumbnailObj.asset_id)}/",
        "creator": GetCreatorOfAsset(AssetThumbnailObj.asset)
    }])
    db.session.commit()
    return redirect("/admin/pending-assets")

@AdminRoute.route("/manage-users", methods=['GET'])
def ManageUsers():
    AdminPermissionRequired('ManageUsers')
    
    query = request.args.get(key = "query", default = None, type = str)
    page = request.args.get(key = "page", default = 1, type = int)
    searchType = request.args.get(key = "searchType", default = "userid", type = str) # userid, username
    orderBy = request.args.get(key = "orderBy", default = "userid", type = str) # userid, creation, lastonline, robux, tix
    orderType = request.args.get(key = "orderType", default = "asc", type = str) # asc, desc

    if searchType not in ["userid", "username", "discordid"]:
        searchType = "userid"
    if orderBy not in ["userid", "creation", "lastonline", "robux", "tix"]:
        orderBy = "userid"
    if orderType not in ["asc", "desc"]:
        orderType = "desc"
    
    if type(query) is not str:
        query = None


    def CreateUserInfo( userObj : User ):
        userEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=userObj.id).first()
        if userEconomyObj is None:
            userEconomyObj = UserEconomy(userid=userObj.id)
            db.session.add(userEconomyObj)
            db.session.commit()
        return {
            "id": userObj.id,
            "username": userObj.username,
            "creation": userObj.created,
            "lastonline": userObj.lastonline,
            "robux": userEconomyObj.robux,
            "tix": userEconomyObj.tix,
            "accountstatus": userObj.accountstatus
        }

    returnList = []

    if query is not None:
        if searchType == "userid":
            userObject : list[User] = [User.query.filter_by(id=int(query)).first()]
        elif searchType == "username":
            userObject : list[User] = User.query.filter(User.username.ilike(f"%{query}%")).offset((page-1)*15).limit(15).all()
        elif searchType == "discordid":
            LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(discord_id=int(query)).first()
            if LinkedDiscordObj is not None:
                userObject = [User.query.filter_by(id=LinkedDiscordObj.user_id).first()]
            else:
                userObject = []
        
        for userObj in userObject:
            if userObj is None:
                continue
            if len(returnList) >= 15:
                break
            returnList.append(CreateUserInfo(userObj))
    else:
        UserQuery = None
        if orderBy == "userid":
            UserQuery = User.id
        elif orderBy == "creation":
            UserQuery = User.created
        elif orderBy == "lastonline":
            UserQuery = User.lastonline
        elif orderBy == "robux":
            UserQuery = UserEconomy.robux
        elif orderBy == "tix":
            UserQuery = UserEconomy.tix
        
        if orderType == "asc":
            UserQuery = UserQuery.asc()
        elif orderType == "desc":
            UserQuery = UserQuery.desc()
        
        # If we are ordering by robux or tix, we need to join the UserEconomy table and only select the userEconomy column for that user by selecting its id
        if orderBy in ["robux", "tix"]:
            UserQuery = User.query.join(UserEconomy, User.id == UserEconomy.userid).order_by(UserQuery)
        else:
            UserQuery = User.query.order_by(UserQuery)
        
        UserQuery = UserQuery.paginate(page=page, per_page=15, error_out=False)
        for userObj in UserQuery.items:
            returnList.append(CreateUserInfo(userObj))
    
    isThereNextPage = False
    if len(returnList) == 15:
        isThereNextPage = True

    return render_template("admin/usermanage/search.html",
                            query=query,
                            searchType=searchType,
                            orderBy=orderBy,
                            orderType=orderType,
                            returnList=returnList,
                            isThereNextPage=isThereNextPage,
                            page=page
                           )

@AdminRoute.route("/manage-users", methods=['POST'])
def ManageUserQuery():
    AdminPermissionRequired('ManageUsers')

    query = request.form.get(key = "user-search-input", default=None, type=str)
    searchType = request.form.get(key = "user-search-type", default = "userid", type = str) # userid, username
    orderBy = request.form.get(key = "user-order-by", default = "userid", type = str) # userid, creation, lastonline, robux, tix
    orderType = request.form.get(key = "user-order-direction", default = "asc", type = str) # asc, desc

    if searchType not in ["userid", "username", "discordid"]:
        searchType = "userid"
    if orderBy not in ["userid", "creation", "lastonline", "robux", "tix"]:
        orderBy = "userid"
    if orderType not in ["asc", "desc"]:
        orderType = "desc"
    
    if searchType == "userid" and query != "":
        try:
            query = int(query)
        except:
            searchType = "username"
    if query == "":
        query = None
    
    if query is None:
        return redirect(f"/admin/manage-users?searchType={searchType}&orderBy={orderBy}&orderType={orderType}")
    else:
        return redirect(f"/admin/manage-users?query={query}&searchType={searchType}&orderBy={orderBy}&orderType={orderType}")

@AdminRoute.route("/manage-users/<int:userid>", methods=['GET'])
def ManageUser(userid : int):
    AdminPermissionRequired('ManageUsers')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    isAdministrator = AdminPermissions.query.filter_by(userid=userid).first() is not None
    TotalVisits = 0
    UniverseList : list[Universe] = Universe.query.filter_by( creator_id = userObj.id, creator_type = 0 ).all()
    for UniverseObj in UniverseList:
        TotalVisits += UniverseObj.visit_count
    UserEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=userObj.id).first()
    if UserEconomyObj is None:
        UserEconomyObj = UserEconomy(userid=userObj.id)
        db.session.add(UserEconomyObj)
        db.session.commit()
    DescriptionLines = userObj.description.split("\n")
    if HasAdminPermission('ViewLoginHistoryDetailed'):
        LastLogin : LoginRecord = LoginRecord.query.filter_by(userid=userObj.id).order_by(LoginRecord.timestamp.desc()).first()
    else:
        LastLogin = None
    
    LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=userObj.id).first()
    if LinkedDiscordObj is not None:
        DiscordUserInfo : discord.DiscordUserInfo = discord.DiscordUserInfo(
            UserId = LinkedDiscordObj.discord_id,
            Username = LinkedDiscordObj.discord_username,
            Discriminator = LinkedDiscordObj.discord_discriminator,
            AvatarHash = LinkedDiscordObj.discord_avatar,
        )
    else:
        DiscordUserInfo = None

    InviteKeyUsed : InviteKey = InviteKey.query.filter_by(used_by=userObj.id).first()
    LastestUserBanObj : UserBan = UserBan.query.filter_by(userid=userObj.id, acknowledged = False).order_by(UserBan.id.desc()).first()
    
    return render_template(
        "admin/usermanage/view.html",
        userObj=userObj,
        isAdministrator=isAdministrator,
        TotalVisits=TotalVisits,
        UserEconomyObj=UserEconomyObj,
        DescriptionLines=DescriptionLines,
        LastLogin=LastLogin,
        LinkedDiscordObj=LinkedDiscordObj,
        DiscordUserInfo=DiscordUserInfo,
        LastestUserBanObj=LastestUserBanObj,
        InviteKeyUsed=InviteKeyUsed,
        HasAdminPermission=HasAdminPermission
    )

@AdminRoute.route("/manage-users/<int:userid>/reset-username", methods=['POST'])
def ResetUserUsername(userid: int):
    AdminPermissionRequired('ManageUsers')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    new_username = f"NEXIUM-user-{userObj.id}"
    userObj.username = new_username
    db.session.commit()

    flash(f"Username reset to {new_username}", "success")
    return redirect(f"/admin/manage-users/{userObj.id}")

@AdminRoute.route("/manage-users/<int:userid>/reset-bio", methods=['POST'])
def ResetUserBio(userid: int):
    AdminPermissionRequired('ManageUsers')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    userObj.description = "[ Content Deleted ]"
    db.session.commit()

    flash("User bio reset", "success")
    return redirect(f"/admin/manage-users/{userObj.id}")

@AdminRoute.route("/manage-users/<int:userid>/reset-password", methods=['POST'])
def ResetUserPassword(userid: int):
    AdminPermissionRequired('ManageUsers')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    NewPassword = secrets.token_urlsafe(12)
    auth.SetPassword(userObj, NewPassword)

    UserLoginRecords : list[LoginRecord] = LoginRecord.query.filter_by(userid=userid).all()
    for LoginRecordObj in UserLoginRecords:
        if LoginRecordObj.session_token:
            auth.invalidateToken(LoginRecordObj.session_token)

    flash(f"Password reset to: {NewPassword}", "success")
    return redirect(f"/admin/manage-users/{userObj.id}")

@AdminRoute.route("/manage-users/<int:userid>/manage-admin-permissions", methods=["GET"])
def ManageUserAdminPermissions( userid : int ):
    AdminPermissionRequired('ManageAdminPermissions')
    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    return render_template(
        "admin/usermanage/manage-admin-perms.html",
        userObj = userObj
    )

@AdminRoute.route("/manage-users/<int:userid>/manage-admin-permissions/api/fetch-permissions", methods=["GET"])
def ManageUserAdminPermissionsFetchPermissions( userid : int ):
    AdminPermissionRequired('ManageAdminPermissions')
    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    PermissionsData : list[dict] = []

    for permission in PermissionsDefinition:
        permissionData = PermissionsDefinition[permission]
        PermissionsData.append({
            "internal_name": permission,
            "friendly_name": permissionData["Name"],
            "description": permissionData["Description"],
            "is_hidden": permissionData["Hidden"] if "Hidden" in permissionData else False,
            "hasPermission": AdminPermissions.query.filter_by(userid=userObj.id, permission=permission).first() is not None,
            "bi_icon": permissionData["icon"] if "icon" in permissionData else None
        })
    
    data_response = make_response(json.dumps(PermissionsData))
    data_response.headers['Content-Type'] = 'application/json'
    data_response.headers['Cache-Control'] = 'no-cache'
    return data_response

@AdminRoute.route("/manage-users/<int:userid>/manage-admin-permissions/api/set-permissions", methods=["POST"])
@limiter.limit("30/minute")
def ManageUserAdminPermissionsSetPermission(userid: int):
    AdminPermissionRequired('ManageAdminPermissions')
    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    
    UpdatedPermissionData = request.json

    # Check for permissions data in the request
    if "permissions" not in UpdatedPermissionData:
        return jsonify({
            "success": False,
            "reason": "Bad Data: No permissions provided."
        }), 400  # Use 400 for bad requests

    # Validate each permission in the provided list
    for permission in UpdatedPermissionData["permissions"]:
        if permission not in PermissionsDefinition:
            return jsonify({
                "success": False,
                "reason": f"Unknown permission {permission}"
            }), 400

    # Remove existing permissions for the user
    AdminPermissions.query.filter_by(userid=userObj.id).delete()
    db.session.commit()

    # Add new permissions
    for permission in UpdatedPermissionData["permissions"]:
        db.session.add(AdminPermissions(userid=userObj.id, permission=permission))
    db.session.commit()

    return jsonify({
        "success": True,
        "reason": ""
    }), 200


@AdminRoute.route("/manage-users/<int:userid>/ban-history", methods=['GET'])
def ManageUserBanHistory( userid : int ):
    AdminPermissionRequired('ManageUsers')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    UserBanHistory : list[UserBan] = UserBan.query.filter_by(userid=userObj.id).order_by(UserBan.id.desc()).paginate(page=1, per_page=10, error_out=False)
    def GetBanAuthorName( banObj : UserBan ):
        Author : User = User.query.filter_by(id=banObj.author_userid).first()
        if Author is None:
            return "Unknown"
        return Author.username
    return render_template(
        "admin/usermanage/banhistory.html",
        userObj = userObj,
        BanHistory = UserBanHistory,
        GetBanAuthorName = GetBanAuthorName
    )

@AdminRoute.route("/manage-users/<int:userid>/invite-keys", methods=['GET'])
def ManageUserInviteKeysview(userid : int):
    AdminPermissionRequired('ManageUsers')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    PageNumber = request.args.get( key = "page", default = 1, type = int )
    if PageNumber < 1:
        PageNumber = 1

    UserInviteKeys = InviteKey.query.filter_by(created_by = userObj.id).order_by(InviteKey.created_at.desc()).paginate( page = PageNumber, per_page = 15, error_out = False )
    return render_template(
        "admin/usermanage/invitekeys.html",
        userObj = userObj,
        InviteKeys = UserInviteKeys
    )

def LogUserBanAction( BannedUser : User, Actor : User, BanObject : UserBan ):
    BanEmbed = {
        "type": "rich",
        "title": f"{BannedUser.username} ({BannedUser.id}) Banned",
        "description": "",
        "color": 0xef0000,
        "fields": [
            {
                "name": "Ban Author",
                "value": f"[{Actor.username}]({config.BaseURL}/admin/manage-users/{Actor.id}) ({Actor.id})",
                "inline": False
            },
            {
                "name": "Banned User",
                "value": f"[{BannedUser.username}]({config.BaseURL}/admin/manage-users/{BannedUser.id}) ({BannedUser.id})",
                "inline": False
            },
            {
                "name": "Ban Type",
                "value": f"{BanObject.ban_type.name}",
                "inline": False
            },
            {
                "name": "Ban Reason",
                "value": f"{BanObject.reason}",
                "inline": False
            },
            {
                "name": "Internal Reason",
                "value": f"{BanObject.moderator_note}",
                "inline": False
            },
            {
                "name": "Expiration",
                "value": f"{BanObject.expires_at}",
                "inline": False
            }
        ],
        "author": {
            "name": BannedUser.username,
            "icon_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(BannedUser.id)}"
        },
        "footer": {
            "text": "NEXIUM - Moderation Logs"
        },
        "timestamp": datetime.utcnow().isoformat()
    }
    def thread_func():
        try:
            requests.post(
                url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
                json = {
                    "username": "NEXIUM - Moderation Logs",
                    "embeds": [BanEmbed],
                    "avatar_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(BannedUser.id)}"
                },
                timeout = 15 # without timeout the thread will hang if request hangs
            )
        except Exception as e:
            logging.warn(f"Admin > LogUserBanAction: Exception raised when sending webhook - {str(e)}")
    
    threading.Thread(target=thread_func).start()

def LogUserUnbanAction( TargetUser : User, Actor : User, BanObj : UserBan ):
    Author : User = User.query.filter_by( id = BanObj.author_userid ).first()
    BanEmbed = {
        "type": "rich",
        "title": f"{TargetUser.username} ({TargetUser.id}) Unbanned",
        "description": "",
        "color": 0xfcdb03,
        "fields": [
            {
                "name": "Ban Author",
                "value": f"[{Author.username}]({config.BaseURL}/admin/manage-users/{Author.id}) ({Author.id})",
                "inline": False
            },
            {
                "name": "Unbanned By",
                "value": f"[{Actor.username}]({config.BaseURL}/admin/manage-users/{Actor.id}) ({Actor.id})",
                "inline": False
            },
            {
                "name": "Banned User",
                "value": f"[{TargetUser.username}]({config.BaseURL}/admin/manage-users/{TargetUser.id}) ({TargetUser.id})",
                "inline": False
            },
            {
                "name": "Ban Type",
                "value": f"{BanObj.ban_type.name}",
                "inline": False
            },
            {
                "name": "Ban Reason",
                "value": f"{BanObj.reason}",
                "inline": False
            },
            {
                "name": "Internal Reason",
                "value": f"{BanObj.moderator_note}",
                "inline": False
            },
            {
                "name": "Expiration",
                "value": f"{BanObj.expires_at}",
                "inline": False
            }
        ],
        "author": {
            "name": TargetUser.username,
            "icon_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(TargetUser.id)}"
        },
        "footer": {
            "text": "NEXIUM - Moderation Logs"
        },
        "timestamp": datetime.utcnow().isoformat()
    }

    def thread_func():
        try:
            requests.post(
                url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
                json = {
                    "username": "NEXIUM - Moderation Logs",
                    "embeds": [BanEmbed],
                    "avatar_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(TargetUser.id)}"
                },
                timeout = 15 # without timeout the thread will hang if request hangs
            )
        except Exception as e:
            logging.warn(f"Admin > LogUserUnbanAction: Exception raised when sending webhook - {str(e)}")
    
    threading.Thread(target=thread_func).start()

@AdminRoute.route("/manage-users/<int:userid>/ban-user", methods=['GET'])
def BanUser(userid: int):
    AdminPermissionRequired('ManageUsers')
    AdminPermissionRequired('BanUser')

    userObj = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    isAdministrator = AdminPermissions.query.filter_by(userid=userid).first() is not None  # Check if they have at least one admin permission
    if isAdministrator:
        flash("You cannot ban an administrator", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}")

    LastestUserBanObj = UserBan.query.filter_by(userid=userObj.id, acknowledged=False).order_by(UserBan.id.desc()).first()

    return render_template("admin/usermanage/ban.html", userObj=userObj, LastestUserBanObj=LastestUserBanObj)

@AdminRoute.route("/manage-users/<int:userid>/ban-user", methods=['POST'])
def BanUserPost(userid: int):
    AdminPermissionRequired('ManageUsers')
    AdminPermissionRequired('BanUser')
    AuthenticatedUser = auth.GetCurrentUser()
    userObj = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    isAdministrator = AdminPermissions.query.filter_by(userid=userid).first() is not None  # Check if they have at least one admin permission
    if isAdministrator:
        flash("You cannot ban an administrator", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    LastestUserBanObj = UserBan.query.filter_by(userid=userObj.id, acknowledged=False).order_by(UserBan.id.desc()).first()
    if LastestUserBanObj is not None:
        flash("User is already banned", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    BanTypeInput = request.form.get(key='ban_type', default=None, type=int)
    BanReason = request.form.get(key='ban_reason', default=None, type=str)
    ModeratorNote = request.form.get(key='ban_notes', default=None, type=str)

    if BanTypeInput is None or BanTypeInput not in [0, 1, 2, 3, 4, 5, 6] or BanReason is None or ModeratorNote is None:
        flash("Invalid request", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    BanTypeInput = BanType(BanTypeInput)
    if len(BanReason) > 512:
        flash("Ban reason is too long", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
    if len(ModeratorNote) > 512:
        flash("Moderator note is too long", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
    if len(BanReason) < 10:
        flash("Ban reason is too short", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
    if len(ModeratorNote) < 10:
        flash("Moderator note is too short", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    if redis_controller.exists(f"admin_ban_cooldown:{AuthenticatedUser.id}"):
        flash("You are banning users too fast!", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    BanTypeToTimeLength = {
        BanType.Warning: timedelta(days=0),
        BanType.Day1Ban: timedelta(days=1),
        BanType.Day3Ban: timedelta(days=3),
        BanType.Day7Ban: timedelta(days=7),
        BanType.Day14Ban: timedelta(days=14),
        BanType.Day30Ban: timedelta(days=30)
    }
    ExpirationDate = None
    if BanTypeInput in BanTypeToTimeLength:
        ExpirationDate = datetime.utcnow() + BanTypeToTimeLength[BanTypeInput]

    BanObj = UserBan(
        userid=userObj.id,
        author_userid=AuthenticatedUser.id,
        ban_type=BanTypeInput,
        reason=BanReason,
        moderator_note=ModeratorNote,
        expires_at=ExpirationDate
    )
    db.session.add(BanObj)
    if BanTypeInput == BanType.Deleted:
        userObj.accountstatus = 3
    else:
        userObj.accountstatus = 2
    db.session.commit()
    LogUserBanAction(userObj, AuthenticatedUser, BanObj)
    redis_controller.set(f"admin_ban_cooldown:{AuthenticatedUser.id}", "1", ex=15)
    flash("User banned", "success")

    PlaceserverPlayerObj = PlaceServerPlayer.query.filter_by(userid=userObj.id).first()
    if PlaceserverPlayerObj is not None:
        CurrentPlaceServerObj = PlaceServer.query.filter_by(serveruuid=PlaceserverPlayerObj.serveruuid).first()
        EvictPlayer(CurrentPlaceServerObj, userObj.id)

    return redirect(f"/admin/manage-users/{str(userid)}")

@AdminRoute.route("/manage-users/<int:userid>/ban-user/revoke-ban", methods=["POST"])
def UnbanUserPost( userid : int):
    AdminPermissionRequired('ManageUsers')
    AdminPermissionRequired('BanUser')
    AuthenticatedUser : User = auth.GetCurrentUser()
    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    
    banid = request.args.get( key = "banid", default = None, type = int )
    if banid is None:
        flash("Invalid BanID", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    if userObj.accountstatus == 1:
        flash("User does not have an active ban", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
    if userObj.accountstatus == 4:
        flash("User is forgotten, cannot revoke any bans", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    TargetBanObj : UserBan = UserBan.query.filter_by( id = banid, userid = userid ).first()
    if TargetBanObj is None:
        flash("Invalid BanID", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")
    
    if TargetBanObj.acknowledged:
        flash("Ban has already been acknowledged by user", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    if redis_controller.exists(f"admin_unban_user_cooldown:{AuthenticatedUser.id}"):
        flash("You are unbanning users too quickly!", "danger")
        return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

    LogUserUnbanAction( userObj, AuthenticatedUser, TargetBanObj )

    db.session.delete(TargetBanObj)
    userObj.accountstatus = 1
    db.session.commit()
    redis_controller.set(f"admin_unban_user_cooldown:{AuthenticatedUser.id}", "1", ex=30)

    return redirect(f"/admin/manage-users/{str(userid)}/ban-user")

@AdminRoute.route("/manage-users/<int:userid>/login-history", methods=['GET'])
def ViewUserLoginHistory( userid : int ):
    AdminPermissionRequired('ManageUsers')
    AdminPermissionRequired('ViewUserLoginHistory')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    PageNumber = request.args.get(key = "page", default = 1, type = int)
    if PageNumber < 1:
        PageNumber = 1
    LoginHistory : list[LoginRecord] = LoginRecord.query.filter_by(userid=userObj.id).order_by(LoginRecord.timestamp.desc()).paginate(page=PageNumber, per_page=15, error_out=False)
    
    AllUniqueLoginRecords : list[LoginRecord] = LoginRecord.query.filter_by( userid = userObj.id ).order_by(LoginRecord.ip, LoginRecord.session_token).distinct(LoginRecord.ip, LoginRecord.session_token).all()
    AlreadySearchedIPs : list[str] = []
    AlreadySearchedSessionTokens : list[str] = []
    AlternateAccounts : list[User] = []

    for LoginRecordObj in AllUniqueLoginRecords:
        if LoginRecordObj.ip in AlreadySearchedIPs and LoginRecordObj.session_token in AlreadySearchedSessionTokens:
            continue
        
        AlreadySearchedIPs.append(LoginRecordObj.ip)
        AlreadySearchedSessionTokens.append(LoginRecordObj.session_token)

        MatchingLoginRecords : list[LoginRecord] = LoginRecord.query.filter(
            and_(
                or_(
                    LoginRecord.ip == LoginRecordObj.ip,
                    LoginRecord.session_token == LoginRecordObj.session_token
                ),
                LoginRecord.userid != userObj.id
            )
        ).distinct(LoginRecord.userid).all()

        for MatchingLoginRecord in MatchingLoginRecords:
            MatchingUserObj : User = User.query.filter_by(id=MatchingLoginRecord.userid).first()
            if MatchingUserObj is not None and MatchingUserObj not in AlternateAccounts and MatchingLoginRecord.session_token is not None:
                MatchingUserObj.flags = {
                    "ipmatch" : MatchingLoginRecord.ip == LoginRecordObj.ip,
                    "sessiontokenmatch" : MatchingLoginRecord.session_token == LoginRecordObj.session_token,
                    "useragentmatch" : MatchingLoginRecord.useragent == LoginRecordObj.useragent,
                    "hwidmatch" : False
                }
                AlternateAccounts.append(MatchingUserObj)
            elif MatchingUserObj is not None and MatchingUserObj in AlternateAccounts and MatchingLoginRecord.session_token is not None:
                MatchingUserIndex = AlternateAccounts.index(MatchingUserObj)
                AlternateAccounts[MatchingUserIndex].flags = {
                    "ipmatch" : MatchingLoginRecord.ip == LoginRecordObj.ip or AlternateAccounts[MatchingUserIndex].flags["ipmatch"],
                    "sessiontokenmatch" : MatchingLoginRecord.session_token == LoginRecordObj.session_token or AlternateAccounts[MatchingUserIndex].flags["sessiontokenmatch"],
                    "useragentmatch" : MatchingLoginRecord.useragent == LoginRecordObj.useragent or AlternateAccounts[MatchingUserIndex].flags["useragentmatch"],
                    "hwidmatch" : False
                }
    
    AllUniqueHWIDLogs : list[UserHWIDLog] = UserHWIDLog.query.filter_by( user_id = userObj.id ).order_by(UserHWIDLog.hwid).distinct(UserHWIDLog.hwid).all()
    for HWIDLog in AllUniqueHWIDLogs:
        MatchingHWIDLogs : list[UserHWIDLog] = UserHWIDLog.query.filter(
            and_(
                UserHWIDLog.hwid == HWIDLog.hwid,
                UserHWIDLog.user_id != userObj.id
            )
        ).distinct(UserHWIDLog.user_id).all()
        for MatchingHWIDLog in MatchingHWIDLogs:
            MatchingUserObj : User = User.query.filter_by(id=MatchingHWIDLog.user_id).first()
            if MatchingUserObj is not None and MatchingUserObj not in AlternateAccounts:
                MatchingUserObj.flags = {
                    "ipmatch" : False,
                    "sessiontokenmatch" : False,
                    "useragentmatch" : False,
                    "hwidmatch" : True
                }
                AlternateAccounts.append(MatchingUserObj)
            elif MatchingUserObj is not None and MatchingUserObj in AlternateAccounts:
                MatchingUserIndex = AlternateAccounts.index(MatchingUserObj)
                AlternateAccounts[MatchingUserIndex].flags["hwidmatch"] = True

    canViewSensitiveInfo = HasAdminPermission('ViewLoginHistoryDetailed')

    return render_template("admin/usermanage/loginhistory.html", userObj=userObj, LoginHistory=LoginHistory, AlternateAccounts=AlternateAccounts, canViewSensitiveInfo=canViewSensitiveInfo)

@AdminRoute.route("/manage-users/<int:userid>/game-sessions", methods=['GET'])
def ViewUserGameSessions( userid : int ):
    AdminPermissionRequired('ManageUsers')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    PageNumber = request.args.get(key = "page", default = 1, type = int)
    if PageNumber < 1:
        PageNumber = 1
    SessionsLogs : list[GameSessionLog] = GameSessionLog.query.filter_by( user_id = userObj.id ).order_by(GameSessionLog.joined_at.desc()).paginate(page=PageNumber, per_page=15, error_out=False)

    def get_place_name( place_id : int ):
        PlaceAssetObj : Asset = Asset.query.filter_by( id = place_id ).first()
        if PlaceAssetObj is None:
            return "Unknown Place"
        return PlaceAssetObj.name

    return render_template("admin/usermanage/gamesessions.html", userObj=userObj, GameSessions=SessionsLogs, get_place_name=get_place_name)

CategoryToEnum = {
    "purchase": TransactionType.Purchase,
    "sale": TransactionType.Sale,
    "group-payout": TransactionType.GroupPayout,
    "stipends": TransactionType.BuildersClubStipend,
}

@AdminRoute.route("/manage-users/<int:userid>/transactions", methods=['GET'])
def ViewUserTransactions( userid : int ):
    AdminPermissionRequired('ManageUsers')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    
    CategoryArg = request.args.get('category', default="purchase", type=str)
    if CategoryArg not in CategoryToEnum:
        Category : TransactionType = TransactionType.Purchase
    else:
        Category : TransactionType = CategoryToEnum[CategoryArg]

    PageNumber = request.args.get('page', default=1, type=int)
    if PageNumber < 1:
        PageNumber = 1

    TransactionQuery = UserTransaction.query.filter_by( transaction_type = Category)
    CategoryQueryDict = {
        TransactionType.Purchase: lambda queryObj: queryObj.filter_by(
            sender_id = userObj.id,
            sender_type = 0
        ),
        TransactionType.Sale: lambda queryObj: queryObj.filter_by(
            reciever_id = userObj.id,
            reciever_type = 0
        ),
        TransactionType.GroupPayout: lambda queryObj: queryObj.filter_by(
            reciever_id = userObj.id,
            reciever_type = 0
        ),
        TransactionType.BuildersClubStipend: lambda queryObj: queryObj.filter_by(
            reciever_id = userObj.id,
            reciever_type = 0
        ),
    }
    TransactionQuery = CategoryQueryDict[Category](TransactionQuery)
    TransactionQuery = TransactionQuery.order_by(UserTransaction.created_at.desc())
    TransactionQuery = TransactionQuery.paginate( page=PageNumber, per_page=15, error_out=False )

    FormattedTransactions = []
    for Transaction in TransactionQuery.items:
        Transaction : UserTransaction = Transaction
        TransactionInfo = {}
        #TransactionInfo["source"] = {
        #    "id": Transaction.sender_id if Transaction.sender_id != userObj.id else Transaction.reciever_id,
        #    "type": Transaction.sender_type if Transaction.sender_id != userObj.id else Transaction.reciever_type, # VV I know this is bad but im too lazy to think of another way to do it
        #    "name": ( User.query.filter_by(id = Transaction.sender_id).first().username if Transaction.sender_type != 1 else Group.query.filter_by( id = Transaction.sender_id ) ) if Transaction.sender_id != userObj.id else ( User.query.filter_by(id = Transaction.reciever_id).first().username if Transaction.reciever_type != 1 else Group.query.filter_by( id = Transaction.reciever_id ) ),
        #}
        TransactionInfo["source"] = {
            "id": Transaction.sender_id if Transaction.sender_id != userObj.id or Transaction.sender_type != 0 else Transaction.reciever_id,
            "type": Transaction.sender_type if Transaction.sender_id != userObj.id or Transaction.sender_type != 0 else Transaction.reciever_type, # VV I know this is bad but im too lazy to think of another way to do it
            "name": ( User.query.filter_by(id = Transaction.sender_id).first().username if Transaction.sender_type != 1 else Group.query.filter_by( id = Transaction.sender_id ).first().name ) if Transaction.sender_id != userObj.id or Transaction.sender_type != 0 else ( User.query.filter_by(id = Transaction.reciever_id).first().username if Transaction.reciever_type != 1 else Group.query.filter_by( id = Transaction.reciever_id ).first().name ),
        }
        TransactionInfo["currency_amount"] = Transaction.currency_amount
        TransactionInfo["currency_type"] = Transaction.currency_type
        TransactionInfo["created_at"] = Transaction.created_at.strftime("%d/%m/%Y %H:%M:%S UTC")
        TransactionInfo["custom_text"] = Transaction.custom_text
        if Transaction.assetId:
            TransactionInfo["asset"] = {
                "id": Transaction.assetId,
                "name": Asset.query.filter_by(id = Transaction.assetId).first().name,
            }
        else:
            TransactionInfo["asset"] = None
        FormattedTransactions.append(TransactionInfo)

    return render_template(
        "admin/usermanage/transactions.html",
        userObj = userObj,
        PageCategory = CategoryArg,
        TransactionInfo = FormattedTransactions,
        Pagination = TransactionQuery
    )
from app.util.membership import GetUserMembership, RemoveUserMembership, GiveUserMembership
from app.enums.MembershipType import MembershipType
from datetime import timedelta, datetime

from datetime import datetime, timedelta

@AdminRoute.route("/manage-currency", methods=['GET', 'POST'])
def ManageCurrencyAdmin():
    AdminPermissionRequired('ManageCurrency')

    AuthenticatedUser: User = auth.GetCurrentUser()
    if request.method == 'GET':
        return render_template("admin/managecurrency.html", user=AuthenticatedUser)

    target_user = None
    user_id = request.form.get("userid", type=int)
    username = request.form.get("username", default="", type=str).strip()
    if user_id:
        target_user = User.query.filter_by(id=user_id).first()
    elif username:
        target_user = User.query.filter_by(username=username).first()
    else:
        target_user = AuthenticatedUser

    if target_user is None:
        flash("Target user not found", "danger")
        return render_template("admin/managecurrency.html", user=AuthenticatedUser)

    form_action = request.form.get("form_action", "")
    bc_type = request.form.get("bc_type")
    bc_duration = request.form.get("bc_duration", 0)

    logging.info(f"ManageCurrencyAdmin POST: form_action={form_action}, bc_type={bc_type}, amount={request.form.get('amount')}")

    if form_action == "builders_club" or bc_type:
        return handle_builders_club_update(target_user, bc_type, bc_duration)

    return handle_currency_update(target_user)


@AdminRoute.route("/manage-users/<int:userid>/manage-currency", methods=['GET', 'POST'])
def ManageUserCurrency(userid: int):
    AdminPermissionRequired('ManageCurrency')
    
    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    
    if request.method == 'GET':
        return render_template("admin/usermanage/managecurrency.html", userObj=userObj)
    
    form_action = request.form.get("form_action", "")
    bc_type = request.form.get("bc_type")
    logging.info(f"ManageUserCurrency POST: userid={userid}, form_action={form_action}, bc_type={bc_type}, amount={request.form.get('amount')}")
    
    if form_action == "builders_club" or bc_type:
        return handle_builders_club_update(userObj, bc_type, request.form.get("bc_duration", 0))
    
    return handle_currency_update(userObj)


def handle_currency_update(user: User):
    action = request.form.get("action", "add").strip().lower()
    amount_raw = request.form.get("amount", "")

    if amount_raw in (None, ""):
        flash("Amount is required", "warning")
        return redirect(request.url)

    try:
        amount = int(amount_raw)
    except ValueError:
        flash("Invalid amount entered", "danger")
        return redirect(request.url)

    if amount == 0:
        flash("Amount must not be zero", "warning")
        return redirect(request.url)

    currency_type = request.form.get("currency_type", "robux")
    currency_id = 1 if currency_type == "tix" else 0
    currency_name = "Tix" if currency_type == "tix" else "Robux"

    amount = abs(amount)
    if action == "subtract":
        economy.DecrementTargetBalance(user, amount, currency_id)
        flash(f"Successfully removed {amount} {currency_name} from {user.username}", "success")
    else:
        economy.IncrementTargetBalance(user, amount, currency_id)
        flash(f"Successfully added {amount} {currency_name} to {user.username}", "success")

    return redirect(request.url)


def handle_builders_club_update(user: User, bc_type: str, bc_duration: str):
    if not bc_type:
        flash("No Builders Club action selected", "warning")
        return redirect(request.url)
    if bc_type == "none":
        RemoveUserMembership(user.id)
        flash("Successfully removed Builders Club from the user", "success")
        return redirect(request.url)
    
    membership_map = {
        "classic": MembershipType.BuildersClub,
        "turbo": MembershipType.TurboBuildersClub,
        "outrageous": MembershipType.OutrageousBuildersClub
    }
    
    membership_type = membership_map.get(bc_type)
    if not membership_type:
        flash("Invalid Builders Club type selected", "danger")
        return redirect(request.url)
    
    try:
        duration_days = int(bc_duration)
    except ValueError:
        flash("Invalid Builders Club duration", "danger")
        return redirect(request.url)
    
    if duration_days < 0:
        flash("Builders Club duration cannot be negative", "danger")
        return redirect(request.url)

    duration = timedelta(days=duration_days)
    RemoveUserMembership(user.id)
    try:
        GiveUserMembership(user.id, membership_type, expiration=duration)
    except Exception as e:
        logging.error(f"Failed to update Builders Club for user {user.id}: {e}")
        flash("Failed to update Builders Club membership", "danger")
        return redirect(request.url)

    amount_raw = request.form.get("amount", "")
    if amount_raw not in (None, ""):
        try:
            amount = int(amount_raw)
        except ValueError:
            flash("Invalid amount entered", "danger")
            return redirect(request.url)

        if amount == 0:
            flash("Amount must not be zero", "warning")
            return redirect(request.url)

        currency_type = request.form.get("currency_type", "robux")
        currency_id = 1 if currency_type == "tix" else 0
        currency_name = "Tix" if currency_type == "tix" else "Robux"

        if amount > 0:
            economy.IncrementTargetBalance(user, amount, currency_id)
            flash(f"Successfully added {amount} {currency_name} to {user.username}", "success")
        else:
            economy.DecrementTargetBalance(user, abs(amount), currency_id)
            flash(f"Successfully removed {abs(amount)} {currency_name} from {user.username}", "success")
    else:
        flash(f"Successfully updated Builders Club membership for {user.username}", "success")

    return redirect(request.url)


@AdminRoute.route("/manage-users/<int:userid>/manage-inventory", methods=['GET'])
def ManageUserInventory(userid: int):
    AdminPermissionRequired('ManageInventory')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    PageNumber = request.args.get("page", 1, type=int)
    UserInventory = UserAsset.query.filter_by(userid=userid).order_by(UserAsset.created.desc()).paginate(page=PageNumber, per_page=24, error_out=False)

    return render_template("admin/usermanage/manageinventory.html", userObj=userObj, UserInventory=UserInventory, PageNumber=PageNumber)

@AdminRoute.route("/manage-users/<int:userid>/manage-inventory/give", methods=['POST'])
def GiveUserItem(userid: int):
    AdminPermissionRequired('ManageInventory')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    assetid = request.form.get("assetid", type=int)
    if not assetid:
        flash("Please provide an Asset ID", "warning")
        return redirect(f"/admin/manage-users/{userid}/manage-inventory")

    AssetObj: Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        flash("Asset not found", "danger")
        return redirect(f"/admin/manage-users/{userid}/manage-inventory")

    if AssetObj.is_limited_unique:
        if AssetObj.sale_count >= AssetObj.serial_count:
            flash("This limited item has no serials left to give out", "danger")
            return redirect(f"/admin/manage-users/{userid}/manage-inventory")
        ItemSerial = AssetObj.sale_count + 1
        NewUserAsset = UserAsset(userid=userid, assetid=AssetObj.id, serial=ItemSerial)
        AssetObj.sale_count += 1
        if AssetObj.sale_count >= AssetObj.serial_count:
            AssetObj.is_for_sale = False
    else:
        NewUserAsset = UserAsset(userid=userid, assetid=AssetObj.id)

    db.session.add(NewUserAsset)
    db.session.commit()

    flash(f"Gave {AssetObj.name} to {userObj.username}", "success")
    return redirect(f"/admin/manage-users/{userid}/manage-inventory")

@AdminRoute.route("/manage-users/<int:userid>/manage-inventory/take/<int:uaid>", methods=['POST'])
def TakeUserItem(userid: int, uaid: int):
    AdminPermissionRequired('ManageInventory')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    UserAssetObj: UserAsset = UserAsset.query.filter_by(id=uaid, userid=userid).first()
    if UserAssetObj is None:
        flash("Item not found in this user's inventory", "danger")
        return redirect(f"/admin/manage-users/{userid}/manage-inventory")

    AssetName = UserAssetObj.asset.name if UserAssetObj.asset else "item"

    TargetUserAvatarAsset: UserAvatarAsset = UserAvatarAsset.query.filter_by(user_id=userid, asset_id=UserAssetObj.assetid).first()
    if TargetUserAvatarAsset is not None:
        db.session.delete(TargetUserAvatarAsset)

    db.session.delete(UserAssetObj)
    db.session.commit()

    flash(f"Took {AssetName} away from {userObj.username}", "success")
    return redirect(f"/admin/manage-users/{userid}/manage-inventory")


@AdminRoute.route("/manage-users/<int:userid>/gdpr-delete", methods=["GET"])
def GDPRDeleteUserPage(userid: int):
    AdminPermissionRequired('GDPRDeleteUser')

    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    return render_template("admin/usermanage/gdprdelete.html", userObj=userObj)


@AdminRoute.route("/manage-users/<int:userid>/gdpr-delete", methods=["POST"])
def GDPRDeleteUserPost(userid: int):
    AdminPermissionRequired('GDPRDeleteUser')

    AuthenticatedUser: User = auth.GetCurrentUser()
    userObj: User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)

    if userObj.accountstatus == 4:
        flash("This user has already been forgotten", "danger")
        return redirect(f"/admin/manage-users/{userid}")

    ConfirmUsername = request.form.get("confirm_username", default="", type=str).strip()

    if ConfirmUsername != userObj.username:
        flash("Typed username did not match, nothing was deleted", "danger")
        return redirect(f"/admin/manage-users/{userid}/gdpr-delete")

    if redis_controller.exists(f"admin_gdpr_delete_cooldown:{AuthenticatedUser.id}"):
        flash("You are erasing users too fast!", "danger")
        return redirect(f"/admin/manage-users/{userid}/gdpr-delete")

    OriginalUsername = userObj.username

    # Revoke every session we have a record of for this user
    UserLoginRecords : list[LoginRecord] = LoginRecord.query.filter_by(userid=userid).all()
    for LoginRecordObj in UserLoginRecords:
        if LoginRecordObj.session_token:
            auth.invalidateToken(LoginRecordObj.session_token)
        db.session.delete(LoginRecordObj)

    # Wipe directly identifying data
    for UserHWIDLogObj in UserHWIDLog.query.filter_by(user_id=userid).all():
        db.session.delete(UserHWIDLogObj)
    for PastUsernameObj in PastUsername.query.filter_by(user_id=userid).all():
        db.session.delete(PastUsernameObj)
    for UserEmailObj in UserEmail.query.filter_by(user_id=userid).all():
        db.session.delete(UserEmailObj)

    LinkedDiscordObj : LinkedDiscord = LinkedDiscord.query.filter_by(user_id=userid).first()
    if LinkedDiscordObj is not None:
        db.session.delete(LinkedDiscordObj)

    # Social graph
    FriendRequest.query.filter(or_(FriendRequest.requester_id == userid, FriendRequest.requestee_id == userid)).delete(synchronize_session=False)
    FriendRelationship.query.filter(or_(FriendRelationship.user_id == userid, FriendRelationship.friend_id == userid)).delete(synchronize_session=False)
    FollowRelationship.query.filter(or_(FollowRelationship.followerUserId == userid, FollowRelationship.followeeUserId == userid)).delete(synchronize_session=False)

    # Scrub content this user authored, without breaking the other side of a conversation
    Message.query.filter_by(sender_id=userid).update({"content": "[This message has been removed]", "subject": "[Removed]"}, synchronize_session=False)
    GroupWallPost.query.filter_by(poster_id=userid).delete(synchronize_session=False)
    GroupMember.query.filter_by(user_id=userid).delete(synchronize_session=False)
    GroupJoinRequest.query.filter_by(user_id=userid).delete(synchronize_session=False)

    # Send any eligible (limited) items through the lottery instead of just deleting them, then nuke everything else they own
    db.session.commit()
    DistributedCount, LotteryError = DistributeUserLimitedsToLottery(userObj, AuthenticatedUser, NotifyOriginalOwner=False)
    if LotteryError is not None:
        logging.info(f"GDPR erasure for user {userid}: lottery distribution skipped ({LotteryError}), remaining limiteds will be deleted instead")

    for RemainingUserAssetObj in UserAsset.query.filter_by(userid=userid).all():
        db.session.delete(RemainingUserAssetObj)

    # Reset avatar, wipe the cached render, & kick from any active game server
    for UserAvatarAssetObj in UserAvatarAsset.query.filter_by(user_id=userid).all():
        db.session.delete(UserAvatarAssetObj)
    UserAvatarObj : UserAvatar = UserAvatar.query.filter_by(user_id=userid).first()
    if UserAvatarObj is not None:
        db.session.delete(UserAvatarObj)
    UserThumbnailObj : UserThumbnail = UserThumbnail.query.filter_by(userid=userid).first()
    if UserThumbnailObj is not None:
        db.session.delete(UserThumbnailObj)

    PlaceserverPlayerObj = PlaceServerPlayer.query.filter_by(userid=userid).first()
    if PlaceserverPlayerObj is not None:
        CurrentPlaceServerObj = PlaceServer.query.filter_by(serveruuid=PlaceserverPlayerObj.serveruuid).first()
        EvictPlayer(CurrentPlaceServerObj, userid)

    # Anonymize the account itself and lock it
    userObj.username = f"deleted_user_{userid}"
    userObj.password = secrets.token_hex(64)
    userObj.description = ""
    userObj.TOTPEnabled = False
    userObj.accountstatus = 4

    db.session.commit()

    redis_controller.set(f"admin_gdpr_delete_cooldown:{AuthenticatedUser.id}", "1", ex=30)

    logging.info(f"GDPR erasure performed on user {userid} (formerly '{OriginalUsername}') by admin {AuthenticatedUser.id}")
    flash(f"User {OriginalUsername} has been permanently erased and locked", "success")
    return redirect(f"/admin/manage-users/{userid}")

'''
@AdminRoute.route("/manage-users/<int:userid>/moderator-notes", methods=['GET'])
def ViewUserModeratorNotes( userid : int ):
    AdminPermissionRequired('ManageUsers')

    userObj : User = User.query.filter_by(id=userid).first()
    if userObj is None:
        return abort(404)
    
    PageNumber = request.args.get('page', default=1, type=int)
    if PageNumber < 1:
        PageNumber = 1
    Robux : list[ModeratorNote] = ModeratorNote.query.filter_by( user_id = userObj.id ).order_by(ModeratorNote.created_at.desc()).paginate(page=PageNumber, per_page=15, error_out=False)

    def GetUserName( TargetUserId : int ) -> str:
        return User.query.filter_by( id = TargetUserId ).first().username

    return render_template(
        "admin/usermanage/currency.html",
        userObj = userObj,
        Robux = Robux,
        GetUserName = GetUserName
    )
'''

from app.pages.admin.websitefeaturesdefinition import WebsiteFeaturesDefinition
from app.util.websiteFeatures import GetWebsiteFeature, SetWebsiteFeature

@AdminRoute.route("/manage-website-features", methods=['GET'])
def ManageWebsiteFeatures():
    AdminPermissionRequired('ManageWebsiteFeatures')

    SitesFeaturesStatus = []
    for feature in WebsiteFeaturesDefinition:
        SitesFeaturesStatus.append( {
            "name" : feature["name"],
            "enabled" : GetWebsiteFeature(feature["name"]),
        })

    return render_template("admin/websitefeatures.html", SitesFeaturesStatus=SitesFeaturesStatus)

@AdminRoute.route("/manage-website-features/<featurename>/disable", methods=['POST'])
def ManageWebsiteFeaturesDisable(featurename : str):
    AdminPermissionRequired('ManageWebsiteFeatures')
    SetWebsiteFeature(featurename, False)
    return redirect("/admin/manage-website-features")

@AdminRoute.route("/manage-website-features/<featurename>/enable", methods=['POST'])
def ManageWebsiteFeaturesEnable(featurename : str):
    AdminPermissionRequired('ManageWebsiteFeatures')
    SetWebsiteFeature(featurename, True)
    return redirect("/admin/manage-website-features")

@AdminRoute.route("/create-user", methods=['GET'])
def CreateUser():
    AdminPermissionRequired('CreateUser')
    return render_template("admin/createuser.html")

@AdminRoute.route("/create-user", methods=['POST'])
@limiter.limit("1/second")
def CreateUserPost():
    AdminPermissionRequired('CreateUser')

    Username = request.form.get( key='username', default=None, type=str )
    Password = request.form.get( key='password', default=None, type=str )

    if Username is None or Password is None:
        flash("Invalid request", "error")
        return redirect("/admin/create-user")
    
    from app.pages.signup.signup import isUsernameAllowed
    from app.util.textfilter import FilterText, TextNotAllowedException
    from app.models.user_avatar import UserAvatar
    import hashlib
    from sqlalchemy import func
    
    if len(Password) < 8:
        flash("Password must be at least 8 characters long")
        return redirect("/signup")

    try:
        FilterText( Text = Username, ThrowException = True)
    except TextNotAllowedException:
        flash("Username is not friendly for NEXIUM")
        return redirect("/signup")
    
    UserSignupLock = redislock.acquire_lock("UserSignupLock", acquire_timeout = 20, lock_timeout=1)
    if not UserSignupLock:
        flash("Failed to acquire user signup lock, please contact administrator", "error")
        return redirect("/admin/create-user")
    
    user = User.query.filter(func.lower(User.username) == func.lower(Username)).first()
    if user is not None:
        redislock.release_lock("UserSignupLock", UserSignupLock)
        flash("Username already taken", "error")
        return redirect("/admin/create-user")

    pastUsername = PastUsername.query.filter(func.lower(PastUsername.username) == func.lower(Username)).first()
    if pastUsername is not None:
        redislock.release_lock("UserSignupLock", UserSignupLock)
        flash("Username already taken", "error")
        return redirect("/admin/create-user")
    
    hashedPassword = hashlib.sha512(Password.encode("utf-8")).hexdigest()
    user = User(username=Username, password=hashedPassword, created=datetime.utcnow(), lastonline=datetime.utcnow())
    db.session.add(user)
    db.session.commit()
    userEconomy = UserEconomy(userid=user.id, robux=0, tix=10)
    db.session.add(userEconomy)
    userAvatar = UserAvatar(user_id=user.id)
    db.session.add(userAvatar)
    db.session.commit()
    redislock.release_lock("UserSignupLock", UserSignupLock)
    TakeUserThumbnail(user.id)

    flash("User created", "success")
    return redirect("/admin/create-user")

@AdminRoute.route("/create-giftcard", methods=['GET'])
def CreateGiftcard():
    AdminPermissionRequired('CreateGiftcard')
    return render_template("admin/creategiftcard.html")

@AdminRoute.route("/create-giftcard", methods=['POST'])
def CreateGiftcardPost():
    AdminPermissionRequired('CreateGiftcard')

    GiftcardTypeInput = request.form.get( key='giftcard-type', default=None, type=int )
    CopiesAmount = request.form.get( key='copies', default=None, type=int )
    GiftcardValue = request.form.get( key='value', default=None, type=int )

    if GiftcardTypeInput is None or CopiesAmount is None or GiftcardValue is None:
        flash("Invalid request", "error")
        return redirect("/admin/create-giftcard")
    
    if CopiesAmount < 1 or CopiesAmount > 15:
        flash("Copies value must be between 1 and 15", "error")
        return redirect("/admin/create-giftcard")
    
    if GiftcardTypeInput not in [0,1,2,3,4]:
        flash("Invalid giftcard type", "error")
        return redirect("/admin/create-giftcard")
    
    if GiftcardValue < 1:
        flash("Giftcard value must be at least 1", "error")
        return redirect("/admin/create-giftcard")

    GiftcardTypeInput : GiftcardType = GiftcardType(GiftcardTypeInput)

    if GiftcardTypeInput in [GiftcardType.RobuxCurrency, GiftcardType.TixCurrency]:
        AuthenticatedUser : User = auth.GetCurrentUser()
        if GiftcardValue > 100000000 and AuthenticatedUser.id not in (1):
            flash("Giftcard value must be at most 100000000 (why do u want that much wtf??)", "error")
            return redirect("/admin/create-giftcard")
    elif GiftcardTypeInput in [GiftcardType.Outrageous_BuildersClub, GiftcardType.Turbo_BuildersClub]:
        if GiftcardValue > 12:
            flash("Giftcard value must be at most 12", "error")
            return redirect("/admin/create-giftcard")
    elif GiftcardTypeInput == GiftcardType.Item:
        AssetObj : Asset = Asset.query.filter_by(id=GiftcardValue).first()
        if AssetObj is None:
            flash("Invalid asset", "error")
            return redirect("/admin/create-giftcard")
    else:
        flash("Invalid giftcard type", "error")
        return redirect("/admin/create-giftcard")
    
    AuthenticatedUser : User = auth.GetCurrentUser()

    if redis_controller.get(f"GiftcardCooldown_{AuthenticatedUser.id}") is not None:
        flash("You are on cooldown bruv", "error")
        return redirect("/admin/create-giftcard")
    redis_controller.set(f"GiftcardCooldown_{AuthenticatedUser.id}", "1", ex=60)

    def GenerateCode():
        Code = ""
        for i in range(0, 5):
            Chunk = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(5))
            Code += Chunk
            if i != 4:
                Code += "-"
        return Code
    
    AllCodes = []

    for i in range(0, CopiesAmount):
        Code = GenerateCode()
        GiftcardObj : GiftcardKey = GiftcardKey(key=Code, value=GiftcardValue, type=GiftcardTypeInput)
        db.session.add(GiftcardObj)
        AllCodes.append(Code)
    db.session.commit()

    flash(f"Created {CopiesAmount} giftcards", "success")
    return render_template("admin/creategiftcard.html", codes=AllCodes)

@AdminRoute.route("/update-asset-file", methods=['GET'])
def UpdateAssetFile():
    AdminPermissionRequired('UpdateAssetFile')
    return render_template("admin/updateassetfile.html")
from app.util import s3helper
@AdminRoute.route("/update-asset-file", methods=['POST'])
def UpdateAssetFilePost():
    AdminPermissionRequired('UpdateAssetFile')
    AuthenticatedUser : User = auth.GetCurrentUser()
    AssetID = request.form.get( key='asset-id', default=None, type=int )
    if AssetID is None:
        flash("Invalid request", "error")
        return redirect("/admin/update-asset-file")
    
    AssetObj : Asset = Asset.query.filter_by(id=AssetID).first()
    if AssetObj is None:
        flash("Invalid asset", "error")
        return redirect("/admin/update-asset-file")
    if AssetObj.creator_id not in [1] or AssetObj.creator_type != 0:
        flash("Asset is not owned by NEXIUM", "error")
        return redirect("/admin/update-asset-file")
    if AssetObj.asset_type not in [AssetType(1), AssetType(2), AssetType(3), AssetType(4), AssetType(8), AssetType(11), AssetType(12), AssetType(17), AssetType(18), AssetType(19), AssetType(24), AssetType(27), AssetType(28), AssetType(29), AssetType(30), AssetType(31), AssetType(32), AssetType(41), AssetType(42), AssetType(43), AssetType(44), AssetType(45), AssetType(46), AssetType(47), AssetType(57), AssetType(58)]:
        if AuthenticatedUser.id not in (1):
            flash("You are not allowed to update this type of asset", "error")
            return redirect("/admin/update-asset-file")
    
    if 'file' not in request.files:
        flash("No file uploaded", "error")
        return redirect("/admin/update-asset-file")
    
    AssetFile = request.files['file']
    if AssetFile.filename == '':
        flash("No file uploaded", "error")
        return redirect("/admin/update-asset-file")
    
    AssetFile.seek(0)
    AssetFileContent = AssetFile.read()
    AssetFileHash = hashlib.sha512(AssetFileContent).hexdigest()

    from app.util.assetvalidation import ValidateClothingImage, ValidatePlaceFile

    if AssetObj.asset_type == AssetType.Image:
        isAllowed = ValidateClothingImage( AssetFile, verifyResolution=False)
        if not isAllowed:
            flash("Invalid image", "error")
            return redirect("/admin/update-asset-file")
    else:
        isValidPlaceFile = ValidatePlaceFile( AssetFile)
        if type(isValidPlaceFile) is str:
            flash(f"Validation Failed: {isValidPlaceFile}", "error")
            return redirect("/admin/update-asset-file")
    
    if not s3helper.DoesKeyExist(AssetFileHash):
        s3helper.UploadBytesToS3(AssetFileContent, AssetFileHash)
    
    NewAssetVersion : AssetVersion = assetversion.CreateNewAssetVersion(AssetObj, AssetFileHash)
    AssetObj.updated_at = datetime.utcnow()
    db.session.commit()

    flash("Asset file updated", "success")
    return redirect("/admin/update-asset-file")

@AdminRoute.route("/copy-bundle", methods=['GET'])
def CopyBundle():
    AdminPermissionRequired('CopyBundle')
    return render_template("admin/bundlecopier.html")

@AdminRoute.route("/copy-bundle", methods=['POST'])
def CopyBundlePost():
    AdminPermissionRequired('CopyBundle')

    BundleID = request.form.get( key='bundle-id', default=None, type=int )
    if BundleID is None:
        flash("Invalid request", "error")
        return redirect("/admin/copy-bundle")

    from app.routes.asset import MigrateBundle, MigrateBundleException
    try:
        NewBundle : Asset = MigrateBundle(BundleID)
    except MigrateBundleException as e:
        flash(e.message, "error")
        return redirect("/admin/copy-bundle")
    except Exception as e:
        flash(f"An error occured, {str(e)}", "error")
        return redirect("/admin/copy-bundle")
    
    return redirect(f"/admin/manage-assets/{str(NewBundle.id)}")

@AdminRoute.route("/create-asset", methods=['GET'])
def CreateAsset():
    AdminPermissionRequired('CreateAsset')
    return render_template("admin/createasset.html")

@AdminRoute.route("/create-asset", methods=['POST'])
def CreateAssetPost():
    AdminPermissionRequired('CreateAsset')

    AssetTypeInput = request.form.get( key='asset-type', default=None, type=int )
    AssetName = request.form.get( key='asset-name', default=None, type=str )
    AssetFile = request.files.get(key='file', default=None)

    if AssetTypeInput is None or AssetName is None or AssetFile is None:
        flash("Invalid request", "error")
        return redirect("/admin/create-asset")
    
    if AssetTypeInput not in [8,17,18,19,41,42,43,44,45,46,47,62]:
        flash("Invalid asset type", "error")
        return redirect("/admin/create-asset")
    
    if AssetName == "":
        flash("Asset name cannot be empty", "error")
        return redirect("/admin/create-asset")
    if len(AssetName) > 64:
        flash("Asset name is too long", "error")
        return redirect("/admin/create-asset")

    if AssetFile.content_length > 1024*1024*5:
        flash("File is too big", "error")
        return redirect("/admin/create-asset")
    
    from app.util.assetvalidation import ValidatePlaceFile
    if AssetTypeInput not in [62, 8]:
        isValidBinaryFile = ValidatePlaceFile( AssetFile )
        if isValidBinaryFile != True:
            flash(f"Validation Failed: {isValidBinaryFile}", "error")
            return redirect("/admin/create-asset")
    
    AssetTypeInput : AssetType = AssetType(AssetTypeInput)

    NewAssetObj : Asset = Asset(
        name = AssetName,
        creator_id = 1,
        creator_type = 0,
        asset_type = AssetTypeInput,
        created_at = datetime.utcnow(),
        updated_at = datetime.utcnow(),
        description = "",
        moderation_status = 0,
        asset_genre = 1
    )
    db.session.add(NewAssetObj)
    db.session.commit()

    AssetFile.seek(0)
    AssetFileContent = AssetFile.read()
    AssetFileHash = hashlib.sha512(AssetFileContent).hexdigest()
    s3helper.UploadBytesToS3(AssetFileContent, AssetFileHash)
    NewAssetVersion : AssetVersion = assetversion.CreateNewAssetVersion(NewAssetObj, AssetFileHash)

    return redirect(f"/admin/manage-assets/{str(NewAssetObj.id)}")

@AdminRoute.route("/moderate-asset", methods=['GET'])
def ModerateAsset():
    AdminPermissionRequired('ModerateAsset')
    return render_template("admin/moderateUGC.html")


@AdminRoute.route("/moderate-asset/<int:assetid>", methods=['GET'])
def ModerateAssetView( assetid : int ):
    AdminPermissionRequired('ModerateAsset')

    AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        flash("Invalid asset", "error")
        return redirect("/admin/moderate-asset")
    if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
        if not HasAdminPermission("ManageAsset"):
            flash("You are not allowed to manage a Admin owned asset", "error")
            return redirect("/admin/moderate-asset")
    
    AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=assetid).order_by(AssetThumbnail.id.desc()).all()
    AllAssetVersions : list[AssetVersion] = AssetVersion.query.filter_by(asset_id=assetid).order_by(AssetVersion.id.desc()).all()
    RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
    if AssetObj.creator_type == 0:
        CreatorObj : User = User.query.filter_by(id=AssetObj.creator_id).first()
    else:
        CreatorObj : Group = Group.query.filter_by(id=AssetObj.creator_id).first()

    RelatedAssets : list[Asset] = []
    for assetlink in RelatedAssetsLink:
        if assetlink.ParentAssetId == AssetObj.id:
            RelatedAssets.append(assetlink.ChildAsset)
        else:
            RelatedAssets.append(assetlink.ParentAsset)

    return render_template(
        "admin/moderateUGC.html",
        AssetObj=AssetObj,
        AllAssetThumbnails=AllAssetThumbnails,
        AllAssetVersions=AllAssetVersions,
        CreatorObj=CreatorObj,
        RelatedAssets=RelatedAssets
    )
StatusToColor = {
    0 : {
        "color" : 0x0ef05d,
        "name" : "Approved"
    },
    1 : {
        "color" : 0xffc107,
        "name" : "Pending Review"
    },
    2 : {
        "color" : 0xff0000,
        "name" : "Deleted"
    }
}

def LogAssetModerationAction( Actor : User, AssetObj : Asset ):
    def thread_func():
        EmbedObj = {
            "type": "rich",
            "title": "Asset Moderation",
            "description": f"Asset [{AssetObj.id}](https://www.nexium.fit/admin/moderate-asset/{AssetObj.id}) moderation status has been updated by **{Actor.username}** ({Actor.id})",
            "color": StatusToColor[AssetObj.moderation_status]["color"],
            "fields": [
                {
                    "name": "Asset Type",
                    "value": AssetObj.asset_type.name,
                    "inline": False
                },
                {
                    "name": "Asset Moderation Status",
                    "value": StatusToColor[AssetObj.moderation_status]["name"],
                    "inline": False
                }
            ],
            "author": {
                "name": Actor.username,
                "icon_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
            },
            "footer": {
                "text": "NEXIUM - Moderation Logs"
            },
            "timestamp": datetime.utcnow().isoformat()
        }
        
        try:
            requests.post(
                url = config.DISCORD_ADMIN_LOGS_WEBHOOK,
                json = {
                    "username": "NEXIUM - Moderation Logs",
                    "embeds" : [EmbedObj],
                    "avatar_url": f"https://www.nexium.fit/Thumbs/Head.ashx?x=48&y=48&userId={str(Actor.id)}"
                },
                timeout = 15 # without timeout the thread will hang if request hangs
            )
        except Exception as e:
            logging.warn(f"Admin > LogAssetModerationAction: Exception raised when sending webhook - {str(e)}")
    threading.Thread(target=thread_func).start()

@AdminRoute.route("/moderate-asset/<int:assetid>/quick-delete", methods=['POST'])
def ModerateAssetQuickDelete( assetid : int ):
    AdminPermissionRequired('ModerateAsset')

    AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        flash("Invalid asset", "error")
        return redirect("/admin/moderate-asset")
    if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
        if not HasAdminPermission("ManageAsset"):
            flash("You are not allowed to manage a Admin owned asset", "error")
            return redirect("/admin/moderate-asset")
    if AssetObj.moderation_status == 2:
        flash("Asset is already deleted", "error")
        return redirect(f"/admin/moderate-asset/{str(assetid)}")
    AuthenticatedUser : User = auth.GetCurrentUser()
    if redis_controller.exists(f"ModerateAssetCooldown_{AuthenticatedUser.id}") and AuthenticatedUser.id != 1:
        flash("You are on cooldown", "error")
        return redirect(f"/admin/moderate-asset/{str(assetid)}")
    redis_controller.set(f"ModerateAssetCooldown_{AuthenticatedUser.id}", "1", ex=20)
    
    def ContentDeleteAssetObj( AssetObj : Asset ):
        AssetObj.moderation_status = 2 # Deleted
        AssetObj.name = f"[ Content Deleted {AssetObj.id} ]"
        AssetObj.description = ""
        AssetObj.updated_at = datetime.utcnow()
        AssetObj.is_for_sale = False

        AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).order_by(AssetThumbnail.id.desc()).all()

        for assetthumbnail in AllAssetThumbnails:
            assetthumbnail.moderation_status = 2
            assetthumbnail.updated_at = datetime.utcnow()

        if AssetObj.asset_type == AssetType.Place:
            PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetObj.id).first()
            if PlaceIconObj is not None:
                PlaceIconObj.moderation_status = 2
                PlaceIconObj.updated_at = datetime.utcnow()
        db.session.commit()
    
    ContentDeleteAssetObj(AssetObj)
    RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
    for assetlink in RelatedAssetsLink:
        if assetlink.ParentAssetId == AssetObj.id:
            ContentDeleteAssetObj(assetlink.ChildAsset)
            flash(f"Asset [{assetlink.ChildAssetId}] deleted as it was linked.", "success")
        else:
            ContentDeleteAssetObj(assetlink.ParentAsset)
            flash(f"Asset [{assetlink.ParentAssetId}] deleted as it was linked.", "success")
    
    flash("Asset Content deleted", "success")
    LogAssetModerationAction(AuthenticatedUser, AssetObj)
    return redirect(f"/admin/moderate-asset/{str(assetid)}")

@AdminRoute.route("/moderate-asset/<int:assetid>/quick-approve", methods=['POST'])
def ModerateAssetQuickApporve( assetid : int ):
    AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        flash("Invalid asset", "error")
        return redirect("/admin/moderate-asset")
    if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
        if not HasAdminPermission("ManageAsset"):
            flash("You are not allowed to manage a Admin owned asset", "error")
            return redirect("/admin/moderate-asset")
    if AssetObj.moderation_status == 0:
        flash("Asset is already approved", "error")
        return redirect(f"/admin/moderate-asset/{str(assetid)}")
    
    AuthenticatedUser : User = auth.GetCurrentUser()
    if redis_controller.exists(f"ModerateAssetCooldown_{AuthenticatedUser.id}") and AuthenticatedUser.id != 1:
        flash("You are on cooldown", "error")
        return redirect(f"/admin/moderate-asset/{str(assetid)}")
    redis_controller.set(f"ModerateAssetCooldown_{AuthenticatedUser.id}", "1", ex=20)

    def AllowContentAssetObj( AssetObj : Asset ):
        AssetObj.moderation_status = 0
        AssetObj.updated_at = datetime.utcnow()

        AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).order_by(AssetThumbnail.id.desc()).all()
        for assetthumbnail in AllAssetThumbnails:
            assetthumbnail.moderation_status = 0
            assetthumbnail.updated_at = datetime.utcnow()

        if AssetObj.asset_type == AssetType.Place:
            PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetObj.id).first()
            if PlaceIconObj is not None:
                PlaceIconObj.moderation_status = 0
                PlaceIconObj.updated_at = datetime.utcnow()
        db.session.commit()
    
    AllowContentAssetObj(AssetObj)
    RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
    for assetlink in RelatedAssetsLink:
        if assetlink.ParentAssetId == AssetObj.id:
            AllowContentAssetObj(assetlink.ChildAsset)
            flash(f"Asset [{assetlink.ChildAssetId}] approved as it was linked.", "success")
        else:
            AllowContentAssetObj(assetlink.ParentAsset)
            flash(f"Asset [{assetlink.ParentAssetId}] approved as it was linked.", "success")
    
    flash("Asset approved", "success")
    LogAssetModerationAction(AuthenticatedUser, AssetObj)
    return redirect(f"/admin/moderate-asset/{str(assetid)}")

@AdminRoute.route("/moderate-asset/<int:assetid>/quick-pending", methods=['POST'])
def ModerateAssetQuickPending( assetid : int ):
    AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        flash("Invalid asset", "error")
        return redirect("/admin/moderate-asset")
    if AssetObj.creator_id in [1] and AssetObj.creator_type == 0:
        if not HasAdminPermission("ManageAsset"):
            flash("You are not allowed to manage a Admin owned asset", "error")
            return redirect("/admin/moderate-asset")
    if AssetObj.moderation_status == 1:
        flash("Asset is already pending", "error")
        return redirect(f"/admin/moderate-asset/{str(assetid)}")
    AuthenticatedUser : User = auth.GetCurrentUser()
    if redis_controller.exists(f"ModerateAssetCooldown_{AuthenticatedUser.id}") and AuthenticatedUser.id != 1:
        flash("You are on cooldown", "error")
        return redirect(f"/admin/moderate-asset/{str(assetid)}")
    redis_controller.set(f"ModerateAssetCooldown_{AuthenticatedUser.id}", "1", ex=20)

    def PendingContentAssetObj( AssetObj : Asset ):
        AssetObj.moderation_status = 1
        AssetObj.updated_at = datetime.utcnow()

        AllAssetThumbnails : list[AssetThumbnail] = AssetThumbnail.query.filter_by(asset_id=AssetObj.id).order_by(AssetThumbnail.id.desc()).all()
        for assetthumbnail in AllAssetThumbnails:
            assetthumbnail.moderation_status = 1
            assetthumbnail.updated_at = datetime.utcnow()

        if AssetObj.asset_type == AssetType.Place:
            PlaceIconObj : PlaceIcon = PlaceIcon.query.filter_by(placeid=AssetObj.id).first()
            if PlaceIconObj is not None:
                PlaceIconObj.moderation_status = 1
                PlaceIconObj.updated_at = datetime.utcnow()
        db.session.commit()

    PendingContentAssetObj(AssetObj)
    RelatedAssetsLink : list[AssetModerationLink] = AssetModerationLink.query.filter(or_(AssetModerationLink.ParentAssetId == AssetObj.id, AssetModerationLink.ChildAssetId == AssetObj.id)).all()
    for assetlink in RelatedAssetsLink:
        if assetlink.ParentAssetId == AssetObj.id:
            PendingContentAssetObj(assetlink.ChildAsset)
            flash(f"Asset [{assetlink.ChildAssetId}] pending as it was linked.", "success")
        else:
            PendingContentAssetObj(assetlink.ParentAsset)
            flash(f"Asset [{assetlink.ParentAssetId}] pending as it was linked.", "success")
    
    flash("Asset set to pending", "success")
    LogAssetModerationAction(AuthenticatedUser, AssetObj)
    return redirect(f"/admin/moderate-asset/{str(assetid)}")

@AdminRoute.route("/moderate-asset", methods=['POST'])
def ModerateAssetSearch():
    AdminPermissionRequired('ModerateAsset')

    AssetID = request.form.get( key='asset-id', default=None, type=int )
    if AssetID is None:
        flash("Invalid request", "error")
        return redirect("/admin/moderate-asset")
    if AssetID < 0:
        flash("Invalid request", "error")
        return redirect("/admin/moderate-asset")
    return redirect(f"/admin/moderate-asset/{str(AssetID)}")

@AdminRoute.route("/lottery", methods=["GET"])
def LotteryIndex():
    AdminPermissionRequired('Lottery')

    EligibleUsers = User.query.filter(or_( User.accountstatus == 3, User.accountstatus == 4, User.lastonline < datetime.utcnow() - timedelta( days = 31 ) ))
    EligibleUsers = EligibleUsers.outerjoin( UserAsset, UserAsset.userid == User.id ).outerjoin( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).group_by( User.id ).all()

    def GetTotalLimiteds( UserObject : User ) -> int:
        if redis_controller.exists(f"LotteryLimitedCountCache:{UserObject.id}"):
            return int(redis_controller.get(f"LotteryLimitedCountCache:{UserObject.id}"))
        TotalLimiteds : int = UserAsset.query.filter_by( userid = UserObject.id ).join( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).count()
        redis_controller.set(f"LotteryLimitedCountCache:{UserObject.id}", str(TotalLimiteds), ex=120)
        return TotalLimiteds

    return render_template("admin/lottery/index.html", EligibleUsers=EligibleUsers, GetTotalLimiteds=GetTotalLimiteds, InactiveTime = (datetime.utcnow() - timedelta( days = 31 )) )

def DistributeUserLimitedsToLottery(UserObject: User, ActorUser: User, NotifyOriginalOwner: bool = True):
    """
    Randomly redistributes every limited item owned by UserObject to currently online users.
    Returns (number_of_items_distributed, error_message). error_message is None when nothing went wrong
    (which includes the case where the user simply had no limiteds to give away).
    """
    EligibleLimiteds : list[UserAsset] = UserAsset.query.filter_by( userid = UserObject.id ).join( Asset, Asset.id == UserAsset.assetid ).filter( Asset.is_limited == True ).all()
    if len(EligibleLimiteds) < 1:
        return 0, None

    TotalUsersOnline : int = User.query.filter( User.lastonline > datetime.utcnow() - timedelta( minutes = 2 ) ).count()
    if TotalUsersOnline < 5:
        return 0, "Not enough users online"

    PreviousWinner = None
    DistributedCount = 0
    for UserAssetObj in EligibleLimiteds:
        RandomWinner : User = User.query.filter(and_(
            User.id != UserObject.id,
            User.id != ActorUser.id,
            User.id != PreviousWinner,
            User.lastonline > datetime.utcnow() - timedelta( minutes = 2 )
        )).order_by(func.random()).first()

        if RandomWinner is None:
            return DistributedCount, "Ran out of eligible winners"

        PreviousWinner = RandomWinner.id
        UserAssetObj.userid = RandomWinner.id
        UserAssetObj.updated = datetime.utcnow()
        CreateSystemMessage(
            f"You won a free limited!",
            f"""You have been randomly selected to be given a free limited by NEXIUM.
You have won: {UserAssetObj.asset.name} ( {UserAssetObj.asset.id} ) [ UAID: {UserAssetObj.id} / Serial: { UserAssetObj.serial if UserAssetObj.serial is not None else 'None'} ]

- NEXIUM Team
""",
            RandomWinner.id
        )
        if NotifyOriginalOwner:
            CreateSystemMessage(
                f"Limited Item removed",
                f""""Your limited {UserAssetObj.asset.name} ( {UserAssetObj.asset.id} ) [ UAID: {UserAssetObj.id} / Serial: { UserAssetObj.serial if UserAssetObj.serial is not None else 'None'} ] has been removed from your account and was given to another ca User
This was because you are either:
    - Inactive for over 31 days
    - Terminated

We apologize for any inconvenience this may have caused.

- NEXIUM Team
""",
                UserObject.id
            )

        TargetUserAvatarAsset : UserAvatarAsset = UserAvatarAsset.query.filter_by( user_id = UserObject.id, asset_id = UserAssetObj.asset.id ).first()
        if TargetUserAvatarAsset is not None:
            db.session.delete(TargetUserAvatarAsset)

        db.session.commit()

        try:
            newLimitedTransfer = LimitedItemTransfer(
                original_owner_id = UserObject.id,
                new_owner_id = RandomWinner.id,
                asset_id = UserAssetObj.assetid,
                user_asset_id = UserAssetObj.id,
                transfer_method = LimitedItemTransferMethod.WonByLottery,
                purchased_price = None
            )
            db.session.add(newLimitedTransfer)
            db.session.commit()
        except Exception:
            pass

        DistributedCount += 1

    TakeUserThumbnail( UserObject.id )
    return DistributedCount, None

@AdminRoute.route("/lottery", methods=["POST"])
def LotteryPost():
    AdminPermissionRequired('Lottery')

    SelectedUser : int = request.form.get( key='selected-user', default=None, type=int )
    if SelectedUser is None:
        flash("Invalid request", "error")
        return redirect("/admin/lottery")

    UserObject : User = User.query.filter_by( id = SelectedUser ).first()
    if UserObject is None:
        flash("Invalid user", "error")
        return redirect("/admin/lottery")

    if UserObject.accountstatus not in [3,4] and UserObject.lastonline > datetime.utcnow() - timedelta( days = 31 ):
        flash("User is not eligible", "error")
        return redirect("/admin/lottery")

    AuthenticatedUser : User = auth.GetCurrentUser()

    DistributedCount, ErrorMessage = DistributeUserLimitedsToLottery(UserObject, AuthenticatedUser)
    if DistributedCount == 0:
        flash(ErrorMessage or "User does not have any limiteds", "error")
        return redirect("/admin/lottery")

    flash("Lottery completed", "success")
    return redirect("/admin/lottery")
        
@AdminRoute.route("/generate-invite-keys", methods=['GET'])
def GenerateInviteKeysPage():
    AdminPermissionRequired('GenerateInviteKeys')
    return render_template("admin/generateinvitekeys.html")

@AdminRoute.route("/generate-invite-keys", methods=['POST'])
def GenerateInviteKeysPost():
    AdminPermissionRequired('GenerateInviteKeys')

    AuthenticatedUser : User = auth.GetCurrentUser()

    Amount = request.form.get(key='amount', default=None, type=int)

    if Amount is None:
        flash("Invalid request", "error")
        return redirect("/admin/generate-invite-keys")

    if Amount < 1 or Amount > 50:
        flash("Amount must be between 1 and 50", "error")
        return redirect("/admin/generate-invite-keys")

    if redis_controller.get(f"GenerateInviteKeysCooldown_{AuthenticatedUser.id}") is not None:
        flash("You are on cooldown, please wait before generating more keys.", "error")
        return redirect("/admin/generate-invite-keys")
    redis_controller.set(f"GenerateInviteKeysCooldown_{AuthenticatedUser.id}", "1", ex=60)

    GeneratedKeys = []
    for _ in range(Amount):
        NewKey = invitekeyservice.CreateInviteKey(AuthenticatedUser)
        GeneratedKeys.append(NewKey.key)
    
    flash(f"Successfully generated {Amount} invite key(s).", "success")
    return render_template("admin/generateinvitekeys.html", keys=GeneratedKeys)