from flask import Blueprint, render_template, request, redirect, url_for, flash, abort, after_this_request, Response
from app.util import auth, redislock, websiteFeatures, s3helper, transactions
from app.util.redirect import safe_redirect
from app.util.assetvalidation import ValidateClothingImage, ValidatePlaceFile, ValidateMP3File, ValidateMP3AndConvertToOGG
from app.routes.thumbnailer import TakeThumbnail
from app.models.userassets import UserAsset
from app.util.assetversion import CreateNewAssetVersion, GetLatestAssetVersion
from app.util.textfilter import FilterText
from app.util.membership import GetUserMembership
from app.util.placeinfo import ClearUniversePlayingCountCache, ClearPlayingCountCache
from app.enums.MembershipType import MembershipType
from app.enums.ChatStyle import ChatStyle
from app.models.place import Place
from app.models.asset_version import AssetVersion
from app.models.asset import Asset
from app.models.user import User
from app.models.placeservers import PlaceServer
from app.models.placeserver_players import PlaceServerPlayer
from app.models.gameservers import GameServer
from app.models.asset_moderation_link import AssetModerationLink
from app.extensions import db, limiter, csrf, user_limiter
from app.models.place_icon import PlaceIcon
from app.models.asset_thumbnail import AssetThumbnail
from app.models.gamepass_link import GamepassLink
from app.models.place_developer_product import DeveloperProduct
from app.models.product_receipt import ProductReceipt
from app.models.groups import Group, GroupRole, GroupRolePermission, GroupMember
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.universe import Universe
from app.enums.AssetType import AssetType
from app.enums.TransactionType import TransactionType
from app.enums.PlaceYear import PlaceYear
from app.enums.PlaceRigChoice import PlaceRigChoice
from app.services import economy, groups
from datetime import datetime, timedelta
from io import BytesIO
from config import Config
import requests
import logging
import hashlib
import os
import random
import time
import math

config = Config()

DevelopPagesRoute = Blueprint('DevelopPagesRoute', __name__, template_folder='pages')

def CountAlphanumericCharacters( string : str ):
    count = 0
    for char in string:
        if char.isalnum():
            count += 1
    return count

@DevelopPagesRoute.errorhandler( 429 )
def handle_ratelimit_reached(e):
    flash("You are being rate limited, please try again later", "error")
    return redirect(safe_redirect(fallback_url="/develop"))

@DevelopPagesRoute.route('/develop')
@auth.authenticated_required
def develop():
    PageType = request.args.get('type', default = 9, type = int)
    PageNumber = request.args.get('page', default = 1, type = int)
    GroupIdContext = request.args.get('groupId', default = None, type = int)

    AuthenticatedUser : User = auth.GetCurrentUser()

    CreatorId : int = AuthenticatedUser.id
    CreatorType : int = 0

    if GroupIdContext is not None and GroupIdContext > 0:
        GroupContext : Group = Group.query.filter_by(id=GroupIdContext).first()
        if GroupContext is None:
            return abort(404)
        ViewerGroupRole : GroupRole | None = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupContext )
        if ViewerGroupRole is None:
            return abort(403)
        ViewerRolePermissions : GroupRolePermission = groups.GetRolesetPermission( ViewerGroupRole )
        if not ViewerRolePermissions.manage_items:
            return abort(403)

        CreatorId = GroupContext.id
        CreatorType = 1
    else:
        GroupIdContext = None
    
    UserGroups : list[Group] = Group.query.join(GroupMember, GroupMember.group_id == Group.id).filter(GroupMember.user_id == AuthenticatedUser.id).join(GroupRolePermission, GroupRolePermission.group_role_id == GroupMember.group_role_id).filter(GroupRolePermission.manage_items == True).all()

    PreviousPage = -1
    if PageNumber > 1:
        PreviousPage = PageNumber - 1
    NextPage = -1
    if PageType == 9:
        # Get all the places
        UserPlaces : list[Asset] = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Place ).join( Universe, Universe.root_place_id == Asset.id ).filter( Universe.id != None ).order_by( Universe.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserPlaces.has_next:
            NextPage = PageNumber + 1
        def GetPlaceUniverse( AssetObj : Asset ):
            return Universe.query.filter_by( root_place_id = AssetObj.id ).first()
        return render_template('develop/subpages/games.html', PageType=PageType, UserPlaces=UserPlaces.items, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups, GetPlaceUniverse = GetPlaceUniverse)
    elif PageType == 11:
        # Get all the clothing
        UserClothing : list[Asset] = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Shirt ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserClothing.has_next:
            NextPage = PageNumber + 1
        return render_template('develop/subpages/shirts.html', PageType=PageType, UserClothing=UserClothing.items, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
    elif PageType == 12:
        UserClothing : list[Asset] = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Pants ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserClothing.has_next:
            NextPage = PageNumber + 1
        return render_template('develop/subpages/pants.html', PageType=PageType, UserClothing=UserClothing.items, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
    elif PageType == 2:
        UserClothing = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.TShirt ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserClothing.has_next:
            NextPage = PageNumber + 1
        return render_template('develop/subpages/tshirt.html', PageType=PageType, UserClothing=UserClothing, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
    elif PageType == 24:
        UserAnimation = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Animation ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserAnimation.has_next:
            NextPage = PageNumber + 1
        return render_template('develop/subpages/animation.html', PageType=PageType, UserAnimation=UserAnimation, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
    elif PageType == 3:
        UserSounds = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Audio ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserSounds.has_next:
            NextPage = PageNumber + 1
        return render_template('develop/subpages/sound.html', PageType=PageType, UserSounds=UserSounds, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
    elif PageType == 1:
        UserImages = Asset.query.filter_by( creator_id=CreatorId, creator_type = CreatorType, asset_type=AssetType.Image ).order_by( Asset.updated_at.desc() ).paginate(page = PageNumber, per_page = 10, error_out = False)
        if UserImages.has_next:
            NextPage = PageNumber + 1
        return render_template('develop/subpages/image.html', PageType=PageType, UserImages=UserImages, PreviousPage=PreviousPage, NextPage=NextPage, PageNumber= PageNumber, GroupIdContext = GroupIdContext, UserGroups = UserGroups)
    else:
        return redirect(url_for("DevelopPagesRoute.develop"))

def ValidateAnimationFile(file):
    if not file or not file.filename:
        return False

    allowed_extensions = {'.rbxm', '.rbxmx', '.rbxl', '.rbxlx'}
    ext = os.path.splitext(file.filename.lower())[1]

    if ext not in allowed_extensions:
        return False

    file.seek(0, os.SEEK_END)
    file_size = file.tell()
    file.seek(0)

    if file_size > 5 * 1024 * 1024:
        return False

    return True

@DevelopPagesRoute.route("/develop/create/<int:ReqAssetType>", methods=["POST"])
@auth.authenticated_required
@limiter.limit("15/minute")
@user_limiter.limit("15/minute")
def create(ReqAssetType):
    if ReqAssetType not in [9, 11, 12, 2, 3, 1, 24]:
        flash("Invalid asset type", "error")
        return redirect(url_for("DevelopPagesRoute.develop"))
    if not websiteFeatures.GetWebsiteFeature("AssetCreation"):
        flash("Asset creation is temporarily disabled", "error")
        return redirect(url_for("DevelopPagesRoute.develop"))
    
    AuthenticatedUser : User = auth.GetCurrentUser()
    TargetCreatorObj : User | Group = AuthenticatedUser
    CreatorType = 0

    groupIdContext = request.form.get(key = "groupIdContext", default = None, type = int)
    if groupIdContext is not None and groupIdContext > 0:
        GroupContext : Group = Group.query.filter_by(id=groupIdContext).first()
        if GroupContext is None:
            return abort(404)
        ViewerGroupRole : GroupRole | None = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupContext )
        if ViewerGroupRole is None:
            return abort(403)
        ViewerRolePermissions : GroupRolePermission = groups.GetRolesetPermission( ViewerGroupRole )
        if not ViewerRolePermissions.manage_items:
            return abort(403)
        if not ViewerRolePermissions.create_items:
            flash("You do not have permission to create assets in this group", "error")
            return redirect(url_for("DevelopPagesRoute.develop", groupId=groupIdContext, type=ReqAssetType))
        if ReqAssetType == 9 and not ViewerRolePermissions.manage_group_games:
            flash("You do not have permission to create places in this group", "error")
            return redirect(url_for("DevelopPagesRoute.develop", groupId=groupIdContext, type=ReqAssetType))
        
        TargetCreatorObj = GroupContext
        CreatorType = 1
    
    CreateLockName = f"createasset_{TargetCreatorObj.id}"
    CreateLock = redislock.acquire_lock(CreateLockName, acquire_timeout=10, lock_timeout=1)
    if CreateLock is None:
        flash("You are creating too many assets at once", "error")
        return redirect(url_for("DevelopPagesRoute.develop"))

    @after_this_request
    def handle_group_context( response : Response ):
        if groupIdContext is not None and groupIdContext > 0:
            response = redirect(url_for("DevelopPagesRoute.develop", groupId=groupIdContext, type=ReqAssetType))
        return response

    if ReqAssetType == 9:
        # Check for the amount of places the user has
        AmountOfPlaces = Universe.query.filter_by( creator_id = TargetCreatorObj.id, creator_type = CreatorType ).count()
        MaxPlaces = 2
        if CreatorType == 0:
            UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
            if UserCurrentMembership == MembershipType.BuildersClub:
                MaxPlaces = 6
            elif UserCurrentMembership == MembershipType.TurboBuildersClub:
                MaxPlaces = 12
            elif UserCurrentMembership == MembershipType.OutrageousBuildersClub:
                MaxPlaces = 32
        else:
            MaxPlaces = 10
        
        if AmountOfPlaces >= MaxPlaces:
            redislock.release_lock(CreateLockName, CreateLock)
            flash(f"You can only have {str(MaxPlaces)} places max", "error")
            return redirect(url_for("DevelopPagesRoute.develop"))
        
        HasCreatedPlaceRecently : bool = Universe.query.filter_by( creator_id = TargetCreatorObj.id, creator_type = CreatorType ).filter(Universe.created_at > datetime.utcnow() - timedelta( hours = 1 )).first() is not None
        if HasCreatedPlaceRecently:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("You can only create one place every hour", "error")
            return redirect(url_for("DevelopPagesRoute.develop"))

        NewAsset : Asset = Asset(
            name = f"Untitled Place",
            description = "Check out my new place!",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Place,
            moderation_status=0,
            created_at=datetime.utcnow()
        )
        db.session.add(NewAsset)
        db.session.commit()

        NewPlace : Place = Place(
            placeid = NewAsset.id,
        )
        db.session.add(NewPlace)
        db.session.commit()

        DefaultPlaceFile = open("./app/files/Baseplate.rbxlx", "rb")
        PlaceFileContent = DefaultPlaceFile.read()
        DefaultPlaceFile.close()
        DefaultPlaceFileHash = hashlib.sha512(PlaceFileContent).hexdigest()
        if not s3helper.DoesKeyExist(DefaultPlaceFileHash):
            s3helper.UploadBytesToS3(PlaceFileContent, DefaultPlaceFileHash)
        
        NewAssetVersion : AssetVersion = CreateNewAssetVersion( NewAsset, DefaultPlaceFileHash, UploadedBy = AuthenticatedUser)
        if NewAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewAsset)
            db.session.delete(NewPlace)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop"))

        TakeThumbnail( AssetId=NewAsset.id, isIcon=False )
        TakeThumbnail( AssetId=NewAsset.id, isIcon=True )

        NewUniverse : Universe = Universe(
            root_place_id = NewAsset.id,
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            place_rig_choice = PlaceRigChoice.UserChoice,
            place_year = PlaceYear.Sixteen
        )
        db.session.add(NewUniverse)
        db.session.commit()

        NewPlace.parent_universe_id = NewUniverse.id
        db.session.commit()

        return redirect(url_for("DevelopPagesRoute.develop"))

    if ReqAssetType == 1: #Image
        ImageName = request.form.get("name", default = "Image", type = str)
        if ImageName is None:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        if ImageName == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        if len(ImageName) > 50:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Name is too long, max: 50 characters", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        ImageName = FilterText(ImageName)
        ImageFile = request.files.get("file", default = None)
        ImageObj = ValidateClothingImage( ImageFile, verifyResolution=False, validateFileSize=False, returnImage=True )
        if ImageObj == False:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Invalid image file, Please make sure it is a PNG file and lesser than 3MB", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        if ImageFile.content_length > 1024 * 1024 * 3:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Image file is too large, max: 3MB", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        if ImageObj.size[0] > 2048 or ImageObj.size[1] > 2048:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Image resolution is too large, max: 2048 x 2048", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        
        NewImageAsset : Asset = Asset(
            name = ImageName,
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Image,
            created_at=datetime.utcnow()
        )
        db.session.add(NewImageAsset)
        db.session.commit()

        ImageFile = BytesIO()
        ImageObj.save(ImageFile, format="PNG")

        ImageFile.seek(0)
        ImageFileContent = ImageFile.read()
        ImageFileHash = hashlib.sha512(ImageFileContent).hexdigest()
        if not s3helper.DoesKeyExist(ImageFileHash):
            s3helper.UploadBytesToS3(ImageFileContent, ImageFileHash, contentType="image/png")
        
        NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, ImageFileHash, UploadedBy = AuthenticatedUser)
        if NewImageAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewImageAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=1))
        
        TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )

        return redirect(url_for("DevelopPagesRoute.develop", type=1))

    if ReqAssetType == 11: #shirt
        ShirtName = request.form.get("name", default = "Shirt", type = str)
        if ShirtName is None:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=11))
        if ShirtName == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=11))
        if len(ShirtName) > 50:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Name is too long, max: 50 characters", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=11))
        ShirtName = FilterText(ShirtName)

        ShirtFile = request.files.get("file", default = None)
        isValidClothingFile = ValidateClothingImage( ShirtFile, returnImage = True )
        if not isValidClothingFile:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Invalid shirt file, Please make sure it is a PNG file 585 x 559 and lesser than 1mb", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=11))
        
        NewImageAsset : Asset = Asset(
            name = "Image",
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Image,
            created_at=datetime.utcnow()
        )
        db.session.add(NewImageAsset)
        db.session.commit()

        ShirtFile = BytesIO()
        isValidClothingFile.save(ShirtFile, format="PNG")

        ShirtFile.seek(0)
        ImageFileContent = ShirtFile.read()
        ShirtFileHash = hashlib.sha512(ImageFileContent).hexdigest()
        if not s3helper.DoesKeyExist(ShirtFileHash):
            s3helper.UploadBytesToS3(ImageFileContent, ShirtFileHash, contentType="image/png")
        
        NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, ShirtFileHash, UploadedBy = AuthenticatedUser)
        if NewImageAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewImageAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=11))
        
        TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )

        ShirtTemplateFile = open("./app/files/Shirt.rbxmx", "r")
        ShirtTemplateFileContent = ShirtTemplateFile.read()
        ShirtTemplateFile.close()
        ShirtTemplateFileContent = ShirtTemplateFileContent.format(ShirtImageId = str(NewImageAsset.id))
        ShirtTemplateFileHash = hashlib.sha512(ShirtTemplateFileContent.encode()).hexdigest()
        if not s3helper.DoesKeyExist(ShirtTemplateFileHash):
            s3helper.UploadBytesToS3(ShirtTemplateFileContent, ShirtTemplateFileHash)
        
        NewShirtAsset : Asset = Asset(
            name = ShirtName,
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Shirt,
            created_at=datetime.utcnow()
        )
        db.session.add(NewShirtAsset)
        db.session.commit()

        NewShirtAssetVersion : AssetVersion = CreateNewAssetVersion( NewShirtAsset, ShirtTemplateFileHash, UploadedBy = AuthenticatedUser)
        if NewShirtAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewShirtAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=11))
        
        NewAssetModerationLink : AssetModerationLink = AssetModerationLink(
            ParentAssetId = NewShirtAsset.id,
            ChildAssetId = NewImageAsset.id
        )
        db.session.add(NewAssetModerationLink)

        NewUserAsset : UserAsset = UserAsset(
            userid = AuthenticatedUser.id,
            assetid = NewShirtAsset.id
        )
        db.session.add(NewUserAsset)
        db.session.commit()
        TakeThumbnail( AssetId=NewShirtAsset.id )
        return redirect(url_for("DevelopPagesRoute.develop", type=11))
    
    if ReqAssetType == 12: #pants
        PantsName = request.form.get("name", default = "Pants", type = str)
        if PantsName is None:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        if PantsName == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        if len(PantsName) > 50:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Name is too long, max: 50 characters", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        PantsName = FilterText(PantsName)

        PantsFile = request.files.get("file", default = None)
        isValidClothingFile = ValidateClothingImage( PantsFile, returnImage = True )
        if not isValidClothingFile:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Invalid pants file, Please make sure it is a PNG file 585 x 559 and lesser than 1mb", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        
        NewImageAsset : Asset = Asset(
            name = "Image",
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Image,
            created_at=datetime.utcnow()
        )
        db.session.add(NewImageAsset)
        db.session.commit()

        PantsFile = BytesIO()
        isValidClothingFile.save(PantsFile, format="PNG")

        PantsFile.seek(0)
        ImageFileContent = PantsFile.read()
        PantsFileHash = hashlib.sha512(ImageFileContent).hexdigest()
        if not s3helper.DoesKeyExist(PantsFileHash):
            s3helper.UploadBytesToS3(ImageFileContent, PantsFileHash, contentType="image/png")
        
        NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, PantsFileHash, UploadedBy = AuthenticatedUser)
        if NewImageAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewImageAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        
        TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )

        PantsTemplateFile = open("./app/files/Pants.rbxmx", "r")
        PantsTemplateFileContent = PantsTemplateFile.read()
        PantsTemplateFile.close()
        PantsTemplateFileContent = PantsTemplateFileContent.format(PantsImageId = str(NewImageAsset.id))
        PantsTemplateFileHash = hashlib.sha512(PantsTemplateFileContent.encode()).hexdigest()
        if not s3helper.DoesKeyExist(PantsTemplateFileHash):
            s3helper.UploadBytesToS3(PantsTemplateFileContent, PantsTemplateFileHash)
        
        NewPantsAsset : Asset = Asset(
            name = PantsName,
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Pants,
            created_at=datetime.utcnow()
        )
        db.session.add(NewPantsAsset)
        db.session.commit()

        NewPantsAssetVersion : AssetVersion = CreateNewAssetVersion( NewPantsAsset, PantsTemplateFileHash, UploadedBy = AuthenticatedUser)
        if NewPantsAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewPantsAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        
        NewAssetModerationLink : AssetModerationLink = AssetModerationLink(
            ParentAssetId = NewPantsAsset.id,
            ChildAssetId = NewImageAsset.id
        )
        db.session.add(NewAssetModerationLink)

        NewUserAsset : UserAsset = UserAsset(
            userid = AuthenticatedUser.id,
            assetid = NewPantsAsset.id
        )
        db.session.add(NewUserAsset)
        db.session.commit()
        TakeThumbnail( AssetId=NewPantsAsset.id )
        return redirect(url_for("DevelopPagesRoute.develop", type=12))
    if ReqAssetType == 24:  # Animation
        AnimationName = request.form.get("name", default="Animation", type=str)
    
        if not AnimationName or AnimationName.strip() == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=24))
    
        if len(AnimationName) > 50:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Name is too long, max: 50 characters", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=24))
    
        AnimationName = FilterText(AnimationName)

        AnimationFile = request.files.get("file", default=None)
        if not AnimationFile or AnimationFile.filename == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No file was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=24))

        isValidAnimation = ValidateAnimationFile(AnimationFile)
        if not isValidAnimation:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Invalid animation file. Please upload a valid .rbxm, .rbxmx, or .rbxl file under 5MB.", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=24))

        NewAnimationAsset: Asset = Asset(
            name=AnimationName,
            description="",
            creator_id=TargetCreatorObj.id,
            creator_type=CreatorType,
            asset_type=AssetType.Animation,  
            created_at=datetime.utcnow()
        )
        db.session.add(NewAnimationAsset)
        db.session.commit()

        AnimationFile.seek(0)
        AnimationFileContent = AnimationFile.read()
        AnimationFileHash = hashlib.sha512(AnimationFileContent).hexdigest()

        if not s3helper.DoesKeyExist(AnimationFileHash):
            s3helper.UploadBytesToS3(AnimationFileContent, AnimationFileHash, contentType="application/octet-stream")

        NewAnimationAssetVersion: AssetVersion = CreateNewAssetVersion(
            NewAnimationAsset, 
            AnimationFileHash, 
            UploadedBy=AuthenticatedUser
        )

        if NewAnimationAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewAnimationAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=24))

        NewUserAsset: UserAsset = UserAsset(
            userid=AuthenticatedUser.id,
            assetid=NewAnimationAsset.id
        )
        db.session.add(NewUserAsset)
        db.session.commit()
        return redirect(url_for("DevelopPagesRoute.develop", type=24))
    if ReqAssetType == 2: #tshirt
        TShirtName = request.form.get("name", default = "T-Shirt", type = str)
        if TShirtName is None:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=2))
        if TShirtName == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=2))
        if len(TShirtName) > 50:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Name is too long, max: 50 characters", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=2))
        TShirtName = FilterText(TShirtName)

        TShirtFile = request.files.get("file", default = None)
        isValidClothingFile = ValidateClothingImage( TShirtFile, verifyResolution = False, returnImage = True )
        if not isValidClothingFile:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Invalid TShirt file, Please make sure it is a PNG file and lesser than 1mb", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=2))
        
        NewImageAsset : Asset = Asset(
            name = "Image",
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Image,
            created_at=datetime.utcnow()
        )
        db.session.add(NewImageAsset)
        db.session.commit()

        TShirtFile = BytesIO()
        isValidClothingFile.save(TShirtFile, format="PNG")

        TShirtFile.seek(0)
        ImageFileContent = TShirtFile.read()
        TShirtFileHash = hashlib.sha512(ImageFileContent).hexdigest()
        if not s3helper.DoesKeyExist(TShirtFileHash):
            s3helper.UploadBytesToS3(ImageFileContent, TShirtFileHash, contentType="image/png")
        
        NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAsset, TShirtFileHash, UploadedBy = AuthenticatedUser)
        if NewImageAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewImageAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=12))
        
        TakeThumbnail( AssetId=NewImageAsset.id, isIcon=False )

        TShirtTemplateFile = open("./app/files/TShirt.rbxmx", "r")
        TShirtTemplateFileContent = TShirtTemplateFile.read()
        TShirtTemplateFile.close()
        TShirtTemplateFileContent = TShirtTemplateFileContent.format(TShirtImageId = str(NewImageAsset.id))
        TShirtTemplateFileHash = hashlib.sha512(TShirtTemplateFileContent.encode()).hexdigest()
        if not s3helper.DoesKeyExist(TShirtTemplateFileHash):
            s3helper.UploadBytesToS3(TShirtTemplateFileContent, TShirtTemplateFileHash)
        
        NewTShirtAsset : Asset = Asset(
            name = TShirtName,
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.TShirt,
            created_at=datetime.utcnow()
        )
        db.session.add(NewTShirtAsset)
        db.session.commit()

        NewTShirtAssetVersion : AssetVersion = CreateNewAssetVersion( NewTShirtAsset, TShirtTemplateFileHash, UploadedBy = AuthenticatedUser)
        if NewTShirtAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewTShirtAsset)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=2))
        
        NewAssetModerationLink : AssetModerationLink = AssetModerationLink(
            ParentAssetId = NewTShirtAsset.id,
            ChildAssetId = NewImageAsset.id
        )
        db.session.add(NewAssetModerationLink)

        NewUserAsset : UserAsset = UserAsset(
            userid = AuthenticatedUser.id,
            assetid = NewTShirtAsset.id
        )
        db.session.add(NewUserAsset)
        db.session.commit()
        TakeThumbnail( AssetId=NewTShirtAsset.id )
        return redirect(url_for("DevelopPagesRoute.develop", type=2))


    if ReqAssetType == 3: #sound
        SoundName = request.form.get("name", default = "Sound", type = str)
        if SoundName is None:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        if SoundName == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No name was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        if len(SoundName) > 50:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Name is too long, max: 50 characters", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        SoundName = FilterText(SoundName)
        
        SoundFile = request.files.get("file", default = None)
        if SoundFile is None:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No file was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        if SoundFile.filename == "":
            redislock.release_lock(CreateLockName, CreateLock)
            flash("No file was provided", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        
        # soundDuration = ValidateMP3File( SoundFile )
        # if soundDuration is None:
        #     redislock.release_lock(CreateLockName, CreateLock)
        #     flash("Invalid sound file, Please make sure it is a MP3 file and lesser than 4mb", "error")
        #     return redirect(url_for("DevelopPagesRoute.develop", type=3))


        if SoundFile.content_length > 1024 * 1024 * 8:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Sound file is too large, max: 8MB", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))

        try:
            ConvertedSoundData, soundDuration = ValidateMP3AndConvertToOGG( SoundFile )
        except Exception as e:
            redislock.release_lock(CreateLockName, CreateLock)
            flash(f"Failed to validate file: {str(e)}", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))

        if soundDuration > 60 * 8:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Sound is too long, max: 8 minutes", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))

        soundDurationHalfed = soundDuration
        if soundDuration > 20:
            soundDurationHalfed = 20 +  ((soundDuration - 20) * 0.5)

        
        
        creationCost = math.floor(max(20, soundDurationHalfed))
        robuxBalance, _ = economy.GetUserBalance(AuthenticatedUser)
        if robuxBalance < creationCost:
            redislock.release_lock(CreateLockName, CreateLock)
            flash(f"You do not have enough robux to create this sound, Required: R${str(creationCost)}", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        
        try:
            economy.DecrementTargetBalance(AuthenticatedUser, creationCost, 0)
        except economy.EconomyLockAcquireException:
            redislock.release_lock(CreateLockName, CreateLock)
            flash("Failed to create sound, Please try again later", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        except economy.InsufficientFundsException:
            redislock.release_lock(CreateLockName, CreateLock)
            flash(f"You do not have enough robux to create this sound, Required: R${str(creationCost)}", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        except Exception as e:
            redislock.release_lock(CreateLockName, CreateLock)
            logging.error(f"Failed to create sound, {str(e)}")
            flash("Failed to create sound, Please try again later", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        
        #transactions.CreateTransaction(User.query.filter_by(id=1).first(), AuthenticatedUser, creationCost, 0, TransactionType.Purchase, None, "Created Sound")
        transactions.CreateTransaction(
            Sender = AuthenticatedUser,
            CurrencyAmount = creationCost,
            CurrencyType = 0,
            TransactionType = TransactionType.Purchase,
            CustomText = "Created Sound"
        )

        #SoundFile.seek(0)
        SoundFileContent = ConvertedSoundData
        SoundFileHash = hashlib.sha512(SoundFileContent).hexdigest()

        if not s3helper.DoesKeyExist(SoundFileHash):
            s3helper.UploadBytesToS3(SoundFileContent, SoundFileHash, contentType="audio/ogg")
        
        NewSoundAsset : Asset = Asset(
            name = SoundName,
            description = "",
            creator_id = TargetCreatorObj.id,
            creator_type = CreatorType,
            asset_type = AssetType.Audio,
            created_at=datetime.utcnow()
        )
        db.session.add(NewSoundAsset)
        db.session.commit()

        NewSoundAssetVersion : AssetVersion = CreateNewAssetVersion( NewSoundAsset, SoundFileHash, UploadedBy = AuthenticatedUser)
        if NewSoundAssetVersion is None:
            redislock.release_lock(CreateLockName, CreateLock)
            db.session.delete(NewSoundAsset)
            db.session.commit()
            flash("Failed to create a new asset version, please report this error to our discord server", "error")
            return redirect(url_for("DevelopPagesRoute.develop", type=3))
        
        NewUserAsset : UserAsset = UserAsset(
            userid = AuthenticatedUser.id,
            assetid = NewSoundAsset.id
        )
        db.session.add(NewUserAsset)
        db.session.commit()
        TakeThumbnail( AssetId=NewSoundAsset.id )
        return redirect(url_for("DevelopPagesRoute.develop", type=3))

def isUserAllowedtoViewPage( ViewerUser : User, RelatedObj : Asset | Universe, abortOnFail : bool = True, isGameContext : bool = True ):
    if RelatedObj is None:
        if abortOnFail:
            abort(404)
        else:
            return False
    if RelatedObj.creator_id != ViewerUser.id and RelatedObj.creator_type == 0:
        if abortOnFail:
            abort(404)
        else:
            return False
    elif RelatedObj.creator_type == 1:
        ViewerGroupRole : GroupRole | None = groups.GetUserRolesetInGroup( ViewerUser, RelatedObj.creator_id )
        if ViewerGroupRole is None:
            if abortOnFail:
                abort(404)
            else:
                return False
        ViewerRolePermissions : GroupRolePermission = groups.GetRolesetPermission( ViewerGroupRole )
        if not ViewerRolePermissions.manage_items:
            if abortOnFail:
                abort(404)
            else:
                return False
        if not ViewerRolePermissions.manage_group_games:
            if abortOnFail:
                abort(404)
            else:
                return False
    if type(RelatedObj) != Universe:
        if RelatedObj.asset_type != AssetType.Place and isGameContext:
            if abortOnFail:
                abort(404)
            else:
                return False
    if RelatedObj.moderation_status != 0:
        if abortOnFail:
            abort(403)
        else:
            return False
    
    return True

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/manage", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniversePage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    if request.method == "GET":
        return render_template("develop/universes/manage.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
    else:
        PlaceName : str = request.form.get("name", default="", type=str)
        PlaceDescription : str = request.form.get("description", default="", type=str)
        try:
            AssetYear : PlaceYear = PlaceYear(request.form.get("place_year", default=2018, type=int))
        except ValueError:
            flash("Invalid year", "error")
            return redirect(f"/develop/universes/{universeid}/manage")
        if AssetYear not in [ PlaceYear.Eighteen, PlaceYear.TwentyOne ]:
            flash("Invalid year", "error")
            return redirect(f"/develop/universes/{universeid}/manage")
        if AssetYear in [ PlaceYear.Sixteen ]:
            flash("2016 has been disabled girl", "error")
            return redirect(f"/develop/universes/{universeid}/manage")
        if len(PlaceName) < 3 or len(PlaceName) > 50:
            flash("Place name has to be between 3 to 50 characters long", "error")
            return redirect(f"/develop/universes/{universeid}/manage")
        if len(PlaceDescription) < 3 or len(PlaceDescription) > 700:
            flash("Place description has to be between 3 to 700 characters long", "error")
            return redirect(f"/develop/universes/{universeid}/manage")

        if PlaceDescription.count("\n") > 10:
            flash("Place description can only have 10 or less newlines", "error")
            return redirect(f"/develop/universes/{universeid}/manage")

        if UniverseObj.place_year != AssetYear:
            # we check active servers
            TotalServersActive: int = PlaceServer.query.join(
                Place, Place.placeid == PlaceServer.serverPlaceId
            ).join(
                Universe, Place.parent_universe_id == Universe.id
            ).filter(
                Universe.id == universeid
            ).count()

            if TotalServersActive > 0:
                flash("You cannot change the place year of the universe while there are active servers", "error")
                return redirect(f"/develop/universes/{universeid}/manage")
            UniverseObj.place_year = AssetYear
            
            all_places = Place.query.filter_by(parent_universe_id=universeid).all()
            for place in all_places:
                place.placeyear = AssetYear
                place.updated_at = datetime.utcnow()

            flash("Successfully updated place year", "success")

        PlaceName = FilterText(PlaceName)
        PlaceDescription = FilterText(PlaceDescription)

        RootPlaceAssetObj.name = PlaceName
        RootPlaceAssetObj.description = PlaceDescription
        RootPlaceAssetObj.updated_at = datetime.utcnow()
        UniverseObj.updated_at = datetime.utcnow()

        db.session.commit()

        flash("Successfully updated place", "success")
        return redirect(f"/develop/universes/{universeid}/manage")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/access", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseAccessPage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    if request.method == "GET":
        return render_template("develop/universes/access.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
    else:
        MinimumAccountAge : int = request.form.get("minaccountage", default=0, type=int)
        if MinimumAccountAge < 0 or MinimumAccountAge > 365:
            flash("Minimum account age has to be between 0 to 365", "error")
            return redirect(f"/develop/universes/{universeid}/access")
        isPublic : bool = request.form.get("ispublic") == "on"
        BuildersClubRequired : bool = request.form.get("bcrequired") == "on"
        
        UniverseObj.minimum_account_age = MinimumAccountAge
        UniverseObj.is_public = isPublic
        UniverseObj.bc_required = BuildersClubRequired
        UniverseObj.updated_at = datetime.utcnow()
        db.session.commit()

        flash("Successfully updated place access", "success")
        return redirect(f"/develop/universes/{universeid}/access")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/places", methods=["GET"])
@auth.authenticated_required
def ManageUniversePlacesPage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    TotalAmountPlacesCreated : int = Place.query.filter_by(parent_universe_id = universeid).count()

    universePlaces : list[Place] = Place.query.filter_by(parent_universe_id = universeid).order_by(Place.placeid.asc()).all()

    return render_template("develop/universes/places.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, universePlaces=universePlaces, TotalAmountPlacesCreated=TotalAmountPlacesCreated)

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-place", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniversePlacePage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    TotalAmountPlacesCreated : int = Place.query.filter_by(parent_universe_id = universeid).count()

    if request.method == "GET":
        return render_template("develop/universes/create-place.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, TotalAmountPlacesCreated=TotalAmountPlacesCreated)
    else:
        if TotalAmountPlacesCreated >= 10:
            flash("You can only have 10 places in a universe", "error")
            return redirect(f"/develop/universes/{universeid}/places")
        
        place_creation_lock_name = f"place_creation_lock_{universeid}"
        place_creation_lock = redislock.acquire_lock( lock_name = place_creation_lock_name, acquire_timeout = 5, lock_timeout = 3 )
        if place_creation_lock is None:
            flash("Failed to create place, please try again later", "error")
            return redirect(f"/develop/universes/{universeid}/places")
        
        NewPlaceAsset : Asset = Asset(
            name = f"Untitled Place",
            description = "Check out my new place!",
            creator_id = UniverseObj.creator_id,
            creator_type = UniverseObj.creator_type,
            asset_type = AssetType.Place,
            moderation_status = 0,
            created_at=datetime.utcnow(),
            updated_at=datetime.utcnow()
        )

        db.session.add(NewPlaceAsset)
        db.session.commit()

        NewPlace : Place = Place(
            placeid = NewPlaceAsset.id,
            parent_universe_id = universeid
        )
        db.session.add(NewPlace)
        db.session.commit()

        DefaultPlaceFile = open("./app/files/Baseplate.rbxlx", "rb")
        PlaceFileContent = DefaultPlaceFile.read()
        DefaultPlaceFile.close()
        DefaultPlaceFileHash = hashlib.sha512(PlaceFileContent).hexdigest()
        if not s3helper.DoesKeyExist(DefaultPlaceFileHash):
            s3helper.UploadBytesToS3(PlaceFileContent, DefaultPlaceFileHash)

        redislock.release_lock(place_creation_lock_name, place_creation_lock)

        NewAssetVersion : AssetVersion = CreateNewAssetVersion( NewPlaceAsset, DefaultPlaceFileHash, UploadedBy = AuthenticatedUser)
        if NewAssetVersion is None:
            db.session.delete(NewPlaceAsset)
            db.session.delete(NewPlace)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(f"/develop/universes/{universeid}/places")
        
        TakeThumbnail( AssetId=NewPlaceAsset.id, isIcon=False )
        TakeThumbnail( AssetId=NewPlaceAsset.id, isIcon=True )

        flash("Successfully created a new place", "success")

        return redirect(f"/develop/universes/{universeid}/places")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/gamepasses", methods=["GET"])
@auth.authenticated_required
def ManageUniverseGamepassesPage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    Gamepasses : list[GamepassLink] = GamepassLink.query.filter_by(universe_id = UniverseObj.id).all()

    return render_template("develop/universes/gamepasses.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, Gamepasses=Gamepasses)

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/gamepass/<int:gamepassid>", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseGamepassPage( universeid : int, gamepassid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    GamepassObj : GamepassLink = GamepassLink.query.filter_by( gamepass_id = gamepassid, universe_id = UniverseObj.id ).first()
    if GamepassObj is None:
        abort(404)

    if request.method == "GET":
        return render_template("develop/universes/edit-gamepass.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, GamepassObj=GamepassObj)
    else:
        AssetName = request.form.get("pass-name", default = "", type = str)
        AssetDescription = request.form.get("pass-description", default = "", type = str)
        isForSale = request.form.get("is-for-sale", default = "off") == "on"
        AssetRobuxPrice = request.form.get("robux-cost", default = 0, type = int)

        if AssetName == "":
            flash("Name cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        if len(AssetName) > 35:
            flash("Name cannot be longer than 35 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        if CountAlphanumericCharacters(AssetName) < 3:
            flash("Name must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        
        if AssetDescription == "":
            flash("Description cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        if len(AssetDescription) > 200:
            flash("Description cannot be longer than 200 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        
        if (AssetRobuxPrice < 1 or AssetRobuxPrice > 1000000 ):
            flash("Robux price has to be between 1 to 1,000,000", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
        if UserCurrentMembership == MembershipType.NonBuildersClub and isForSale:
            flash("You must be Builders Club to sell items", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
        
        AssetName = FilterText(AssetName)
        AssetDescription = FilterText(AssetDescription)

        NewIconFile = request.files.get("icon-file", default = None)
        if NewIconFile is not None:
            if NewIconFile.filename != "":
                if NewIconFile.content_length > 1024 * 1024:
                    flash("File size is too big", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
                IconImage = ValidateClothingImage( NewIconFile, verifyResolution=False, validateFileSize=False, returnImage=True )
                if IconImage is False or IconImage is None:
                    flash("Invalid image", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
                if IconImage.width != IconImage.height:
                    flash("Image is not square", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
                if IconImage.width < 128 or IconImage.width > 1024:
                    flash("Image is not between 128x128 and 1024x1024", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
                NewIconFile = BytesIO()
                IconImage.save(NewIconFile, format="PNG")

                NewIconFile.seek(0)
                IconImageHash = hashlib.sha512(NewIconFile.read()).hexdigest()
                if not s3helper.DoesKeyExist(IconImageHash):
                    NewIconFile.seek(0)
                    s3helper.UploadBytesToS3(NewIconFile.read(), IconImageHash, contentType="image/png")
                LatestAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=GamepassObj.gamepass_id).order_by(AssetThumbnail.asset_version_id.desc()).first()
                if LatestAssetThumbnail is None:
                    flash("Failed to get latest asset thumbnail", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
                LatestAssetThumbnail.content_hash = IconImageHash
                LatestAssetThumbnail.created_at = datetime.utcnow()
                LatestAssetThumbnail.moderation_status = 1
                db.session.commit()
                flash("Successfully updated gamepass icon", "success")
    
        GamepassObj.gamepass.name = AssetName
        GamepassObj.gamepass.description = AssetDescription
        GamepassObj.gamepass.is_for_sale = isForSale
        GamepassObj.gamepass.price_robux = AssetRobuxPrice
        GamepassObj.gamepass.updated_at = datetime.utcnow()
        db.session.commit()
        flash("Successfully updated gamepass settings", "success")

        return redirect(f"/develop/universes/{UniverseObj.id}/gamepass/{GamepassObj.gamepass_id}")
    
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-gamepass", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniverseGamepassPage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    if request.method == "GET":
        return render_template("develop/universes/create-gamepass.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
    else:
        gamepassName = request.form.get("name", default="", type=str)
        gamepassDescription = request.form.get("description", default="", type=str)
        file = request.files.get("file", default=None)

        if gamepassName == "":
            flash("Name cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if len(gamepassName) > 35:
            flash("Name cannot be longer than 35 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if CountAlphanumericCharacters(gamepassName) < 3:
            flash("Name must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")

        if gamepassDescription == "":
            flash("Description cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if len(gamepassDescription) > 200:
            flash("Description cannot be longer than 200 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        
        if file is None:
            flash("No file was provided", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if file.filename == "":
            flash("No file was provided", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if file.content_length > 1024 * 1024:
            flash("File size is too big", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        
        GamepassCount : int = GamepassLink.query.filter_by(universe_id = UniverseObj.id).count()
        if GamepassCount >= 15:
            flash("You cannot create more than 15 gamepasses for a place", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        
        FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
        if FileImage is False or FileImage is None:
            flash("Invalid image", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if FileImage.width != FileImage.height:
            flash("Image is not square", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        if FileImage.width < 128 or FileImage.width > 1024:
            flash("Image is not between 128x128 and 1024x1024", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        
        gamepassName = FilterText(gamepassName)
        gamepassDescription = FilterText(gamepassDescription)

        file = BytesIO()
        FileImage.save(file, format="png")

        file.seek(0)
        FileImageHash = hashlib.sha512(file.read()).hexdigest()
        if not s3helper.DoesKeyExist(FileImageHash):
            file.seek(0)
            s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
        
        NewGamepassObj : Asset = Asset(
            name = gamepassName,
            description = gamepassDescription,
            creator_id = UniverseObj.creator_id,
            creator_type = UniverseObj.creator_type,
            asset_type = AssetType.GamePass,
            created_at = datetime.utcnow(),
            updated_at = datetime.utcnow(),
            moderation_status = 0
        )
        db.session.add(NewGamepassObj)
        db.session.commit()

        NewGamepassLink : GamepassLink = GamepassLink(
            place_id = UniverseObj.root_place_id,
            universe_id = UniverseObj.id,
            gamepass_id = NewGamepassObj.id,
            creator_id = AuthenticatedUser.id
        )
        db.session.add(NewGamepassLink)
        db.session.commit()

        EmptyHash = hashlib.sha512(b"").hexdigest()
        NewGamepassVersion : AssetVersion = CreateNewAssetVersion( NewGamepassObj, EmptyHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
        if NewGamepassVersion is None:
            db.session.delete(NewGamepassObj)
            db.session.delete(NewGamepassLink)
            db.session.commit()
            flash("Failed to create a new asset version", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-gamepass")
        
        NewGamepassThumbnail : AssetThumbnail = AssetThumbnail(
            asset_id = NewGamepassObj.id,
            asset_version_id = NewGamepassVersion.version,
            content_hash = FileImageHash,
            created_at = datetime.utcnow(),
            moderation_status = 1
        )
        db.session.add(NewGamepassThumbnail)
        db.session.commit()

        flash("Successfully created gamepass", "success")
        return redirect(f"/develop/universes/{UniverseObj.id}/gamepasses")
    
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/developer-products", methods=["GET"])
@auth.authenticated_required
def ManageUniverseDeveloperProductsPage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    DeveloperProducts : list[DeveloperProduct] = DeveloperProduct.query.filter_by(universe_id = UniverseObj.id).all()

    return render_template("develop/universes/developerproducts.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, DeveloperProducts=DeveloperProducts)

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-product", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniveseDeveloperProductPage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    if request.method == "GET":
        return render_template("develop/universes/create-product.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
    else:
        productName = request.form.get("name", default="", type=str)
        productDescription = request.form.get("description", default="", type=str)
        file = request.files.get("file", default=None)

        if productName == "":
            flash("Name cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if len(productName) > 35:
            flash("Name cannot be longer than 35 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if CountAlphanumericCharacters(productName) < 3:
            flash("Name must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")

        if productDescription == "":
            flash("Description cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if len(productDescription) > 200:
            flash("Description cannot be longer than 200 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        
        if file is None:
            flash("No file was provided", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if file.filename == "":
            flash("No file was provided", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if file.content_length > 1024 * 1024:
            flash("File size is too big", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        
        FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
        if FileImage is False or FileImage is None:
            flash("Invalid image", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if FileImage.width != FileImage.height:
            flash("Image is not square", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        if FileImage.width < 128 or FileImage.width > 1024:
            flash("Image is not between 128x128 and 1024x1024", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        
        productName = FilterText(productName)
        productDescription = FilterText(productDescription)

        file = BytesIO()
        FileImage.save(file, format="png")

        file.seek(0)
        FileImageHash = hashlib.sha512(file.read()).hexdigest()
        if not s3helper.DoesKeyExist(FileImageHash):
            file.seek(0)
            s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")
        
        NewImageAssetObj : Asset = Asset(
            name = "DeveloperProductImage",
            description = "DeveloperProduct icon",
            creator_id = AuthenticatedUser.id,
            creator_type = 0,
            asset_type = AssetType.Image,
            created_at = datetime.utcnow(),
            updated_at = datetime.utcnow(),
            moderation_status = 1
        )
        db.session.add(NewImageAssetObj)
        db.session.commit()

        NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
        if NewImageAssetVersion is None:
            db.session.delete(NewImageAssetObj)
            db.session.commit()
            flash("Failed to create a new asset version, please contact support", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-product")
        
        NewDeveloperProductObj : DeveloperProduct = DeveloperProduct(
            placeid = UniverseObj.root_place_id,
            name = productName,
            description = productDescription,
            iconimage_assetid = NewImageAssetObj.id,
            creator_id = AuthenticatedUser.id,
            universe_id = UniverseObj.id
        )
        TakeThumbnail( AssetId=NewImageAssetObj.id, isIcon=False )

        db.session.add(NewDeveloperProductObj)
        db.session.commit()

        flash("Successfully created developer product", "success")
        return redirect(f"/develop/universes/{UniverseObj.id}/developer-products")
    
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/developer-products/<int:productid>", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseDeveloperProductPage( universeid : int, productid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    DeveloperProductObj : DeveloperProduct = DeveloperProduct.query.filter_by(universe_id = UniverseObj.id, productid = productid).first()
    if DeveloperProductObj is None:
        abort(404)
    
    if request.method == "GET":
        return render_template("develop/universes/edit-product.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, ProductObj=DeveloperProductObj)
    else:
        productName = request.form.get("product-name", default="", type=str)
        productDescription = request.form.get("product-description", default="", type=str)
        file = request.files.get("icon-file", default=None)
        is_for_sale = request.form.get("is-for-sale", default="off") == "on"
        robux_price = request.form.get("robux-cost", default=0, type=int)

        if productName == "":
            flash("Name cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        if len(productName) > 35:
            flash("Name cannot be longer than 35 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        if CountAlphanumericCharacters(productName) < 3:
            flash("Name must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        
        if productDescription == "":
            flash("Description cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        if len(productDescription) > 200:
            flash("Description cannot be longer than 200 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        
        if (robux_price < 1 or robux_price > 1000000 ):
            flash("Robux price has to be between 1 to 1,000,000", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
        if UserCurrentMembership == MembershipType.NonBuildersClub and is_for_sale:
            flash("You must be a Builders Club member to sell items", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
        
        productName = FilterText(productName)
        productDescription = FilterText(productDescription)

        if file is not None:
            if file.filename != "":
                if file.content_length > 1024 * 1024:
                    flash("File size is too big", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
                FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
                if FileImage is False or FileImage is None:
                    flash("Invalid image", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
                if FileImage.width != FileImage.height:
                    flash("Image is not square", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
                if FileImage.width < 128 or FileImage.width > 1024:
                    flash("Image is not between 128x128 and 1024x1024", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
                file = BytesIO()
                FileImage.save(file, format="PNG")
                
                file.seek(0)
                FileImageHash = hashlib.sha512(file.read()).hexdigest()
                if not s3helper.DoesKeyExist(FileImageHash):
                    file.seek(0)
                    s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")

                NewImageAssetObj : Asset = Asset(
                    name = "DeveloperProductImage",
                    description = "DeveloperProduct icon",
                    creator_id = AuthenticatedUser.id,
                    creator_type = 0,
                    asset_type = AssetType.Image,
                    created_at = datetime.utcnow(),
                    updated_at = datetime.utcnow(),
                    moderation_status = 1
                )
                db.session.add(NewImageAssetObj)
                db.session.commit()

                NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
                if NewImageAssetVersion is None:
                    db.session.delete(NewImageAssetObj)
                    db.session.commit()
                    flash("Failed to create a new asset version, please contact support", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")

                TakeThumbnail( AssetId = NewImageAssetObj.id, isIcon=False )
                DeveloperProductObj.iconimage_assetid = NewImageAssetObj.id

                flash("Successfully updated developer product icon", "success")
        
        DeveloperProductObj.name = productName
        DeveloperProductObj.description = productDescription
        DeveloperProductObj.is_for_sale = is_for_sale
        DeveloperProductObj.robux_price = robux_price
        DeveloperProductObj.updated_at = datetime.utcnow()
        db.session.commit()
        flash("Successfully updated developer product settings", "success")

        return redirect(f"/develop/universes/{UniverseObj.id}/developer-products/{DeveloperProductObj.productid}")
    
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/badges", methods=["GET"])
@auth.authenticated_required
def ManageUniverseBadgesPage( universeid : int ):
    from app.pages.games.games import GetTotalBadgeAwardedCount, GetBadgeAwardedPastDay # Avoid circular import

    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    Badges : list[PlaceBadge] = PlaceBadge.query.filter_by(universe_id = UniverseObj.id).all()

    return render_template("develop/universes/badges.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, Badges=Badges, GetTotalBadgeAwardedCount=GetTotalBadgeAwardedCount, GetBadgeAwardedPastDay=GetBadgeAwardedPastDay)

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/create-badge", methods=["GET", "POST"])
@auth.authenticated_required
def CreateUniverseBadgePage( universeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    if request.method == "GET":
        return render_template("develop/universes/create-badge.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj)
    else:
        BadgeName = request.form.get("name", default = "", type = str)
        BadgeDescription = request.form.get("description", default = "", type = str)
        file = request.files.get("icon-file", default=None)

        if BadgeName == "":
            flash("Badge name cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if len(BadgeName) > 35:
            flash("Badge name cannot be longer than 35 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if CountAlphanumericCharacters(BadgeName) < 3:
            flash("Badge name must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        
        if BadgeDescription == "":
            flash("Badge description cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if len(BadgeDescription) > 128:
            flash("Badge description cannot be longer than 128 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if CountAlphanumericCharacters(BadgeDescription) < 3:
            flash("Badge description must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")

        BadgeName = FilterText(BadgeName)
        BadgeDescription = FilterText(BadgeDescription)

        TotalBadgeCount : int = PlaceBadge.query.filter_by(universe_id = UniverseObj.id).count()
        if TotalBadgeCount >= 25:
            flash("You cannot create more than 25 badges for a place", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")

        if file is None:
            flash("No file was provided", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if file.filename == "":
            flash("No file was provided", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if file.content_length > 1024 * 1024:
            flash("File size is too big", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
        if FileImage is False or FileImage is None:
            flash("Invalid image", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if FileImage.width != FileImage.height:
            flash("Image is not square", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        if FileImage.width < 128 or FileImage.width > 1024:
            flash("Image is not between 128x128 and 1024x1024", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")
        file = BytesIO()
        FileImage.save(file, format="PNG")

        file.seek(0)
        FileImageHash = hashlib.sha512(file.read()).hexdigest()
        if not s3helper.DoesKeyExist(FileImageHash):
            file.seek(0)
            s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")

        NewImageAssetObj : Asset = Asset(
            name = "BadgeImage",
            description = "Badge icon",
            creator_id = AuthenticatedUser.id,
            creator_type = 0,
            asset_type = AssetType.Image,
            created_at = datetime.utcnow(),
            updated_at = datetime.utcnow(),
            moderation_status = 1
        )
        db.session.add(NewImageAssetObj)
        db.session.commit()

        NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)

        if NewImageAssetVersion is None:
            db.session.delete(NewImageAssetObj)
            db.session.commit()
            flash("Failed to create a new asset version, please contact support", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/create-badge")

        TakeThumbnail( AssetId = NewImageAssetObj.id, isIcon=False )
        NewBadgeObj : PlaceBadge = PlaceBadge(
            associated_place_id = UniverseObj.root_place_id,
            name = BadgeName,
            description = BadgeDescription,
            icon_image_id = NewImageAssetObj.id,
            universe_id = UniverseObj.id
        )
        db.session.add(NewBadgeObj)
        db.session.commit()

        flash("Successfully created badge", "success")
        return redirect(f"/develop/universes/{UniverseObj.id}/badges")
    
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/badges/<int:badgeid>", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniverseBadgePage( universeid : int, badgeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()

    BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( universe_id = UniverseObj.id, id = badgeid ).first()
    if BadgeObj is None:
        abort(404)

    if request.method == "GET":
        return render_template("develop/universes/edit-badge.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, BadgeObj=BadgeObj)
    else:
        BadgeName = request.form.get("badge-name", default = "", type = str)
        BadgeDescription = request.form.get("badge-description", default = "", type = str)

        if BadgeName == "":
            flash("Badge name cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
        if len(BadgeName) > 35:
            flash("Badge name cannot be longer than 35 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
        if CountAlphanumericCharacters(BadgeName) < 3:
            flash("Badge name must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
        
        if BadgeDescription == "":
            flash("Badge description cannot be empty", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
        if len(BadgeDescription) > 128:
            flash("Badge description cannot be longer than 128 characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
        if CountAlphanumericCharacters(BadgeDescription) < 3:
            flash("Badge description must contain at least 3 alphanumeric characters", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")

        BadgeName = FilterText(BadgeName)
        BadgeDescription = FilterText(BadgeDescription)
        file = request.files.get("icon-file", default=None)

        if file is not None:
            if file.filename != "":
                if file.content_length > 1024 * 1024:
                    flash("File size is too big", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
                FileImage = ValidateClothingImage(file, verifyResolution=False, validateFileSize=False, returnImage=True)
                if FileImage is False or FileImage is None:
                    flash("Invalid image", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
                if FileImage.width != FileImage.height:
                    flash("Image is not square", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
                if FileImage.width < 128 or FileImage.width > 1024:
                    flash("Image is not between 128x128 and 1024x1024", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")
                file = BytesIO()
                FileImage.save(file, format="PNG")
                
                file.seek(0)
                FileImageHash = hashlib.sha512(file.read()).hexdigest()
                if not s3helper.DoesKeyExist(FileImageHash):
                    file.seek(0)
                    s3helper.UploadBytesToS3(file.read(), FileImageHash, contentType="image/png")

                NewImageAssetObj : Asset = Asset(
                    name = "BadgeImage",
                    description = "Badge icon",
                    creator_id = AuthenticatedUser.id,
                    creator_type = 0,
                    asset_type = AssetType.Image,
                    created_at = datetime.utcnow(),
                    updated_at = datetime.utcnow(),
                    moderation_status = 1
                )
                db.session.add(NewImageAssetObj)
                db.session.commit()

                NewImageAssetVersion : AssetVersion = CreateNewAssetVersion( NewImageAssetObj, FileImageHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
                if NewImageAssetVersion is None:
                    db.session.delete(NewImageAssetObj)
                    db.session.commit()
                    flash("Failed to create a new asset version, please contact support", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")

                TakeThumbnail( AssetId = NewImageAssetObj.id, isIcon=False )
                BadgeObj.icon_image_id = NewImageAssetObj.id

                flash("Successfully updated developer product icon", "success")
        
        BadgeObj.name = BadgeName
        BadgeObj.description = BadgeDescription
        BadgeObj.updated_at = datetime.utcnow()

        db.session.commit()
        flash("Successfully updated badge", "success")

        return redirect(f"/develop/universes/{UniverseObj.id}/badges/{BadgeObj.id}")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/manage", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniversePlacePage( universeid : int, placeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
    if PlaceObj is None:
        abort(404)
    PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj

    if request.method == "GET":
        return render_template("develop/games/manage.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj)
    else:
        PlaceName : str = request.form.get("name", default="", type=str)
        PlaceDescription : str = request.form.get("description", default="", type=str)
        try:
            ChatStyleType : ChatStyle = ChatStyle(request.form.get("chat-style-type", default = 2, type = int))
        except ValueError:
            flash("Invalid chat style", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")

        if UniverseObj.place_year in [ PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.TwentyOne ]:
            try:
                AvatarRigType : PlaceRigChoice = PlaceRigChoice(request.form.get("avatar-rig-type", default=0, type=int))
            except ValueError:
                flash("Invalid avatar rig type", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")

        if len(PlaceName) < 3 or len(PlaceName) > 50:
            flash("Place name has to be between 3 to 50 characters long", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
        if len(PlaceDescription) < 3 or len(PlaceDescription) > 700:
            flash("Place description has to be between 3 to 700 characters long", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
        if PlaceDescription.count("\n") > 10:
            flash("Place description can only have 10 or less newlines", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")
        
        PlaceName : str = FilterText(PlaceName)
        PlaceDescription : str = FilterText(PlaceDescription)
        PlaceAssetObj.name = PlaceName
        PlaceAssetObj.description = PlaceDescription
        PlaceObj.chat_style = ChatStyleType
        if UniverseObj.place_year in [ PlaceYear.Eighteen, PlaceYear.Twenty, PlaceYear.TwentyOne ]:
            PlaceObj.rig_choice = AvatarRigType

        PlaceAssetObj.updated_at = datetime.utcnow()
        UniverseObj.updated_at = datetime.utcnow()

        db.session.commit()
        flash("Successfully updated place settings", "success")

        return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/manage")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/access", methods=["GET", "POST"])
@auth.authenticated_required
def ManageUniversePlaceAccessPage( universeid : int, placeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
    if PlaceObj is None:
        abort(404)
    PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj

    if request.method == "GET":
        return render_template("develop/games/access.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj)
    else:
        MaxPlayers : int = request.form.get("maxplayers", default=10, type=int)
        if MaxPlayers < 2 or MaxPlayers > 50:
            flash("Max players has to be between 2 to 50", "error")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/access")
        PlaceObj.maxplayers = MaxPlayers
        db.session.commit()

        flash("Successfully updated place access settings", "success")
        return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/access")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/upload-version", methods=["GET", "POST"])
@auth.authenticated_required
def UploadUniversePlaceVersionPage( universeid : int, placeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
    if PlaceObj is None:
        abort(404)
    PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj

    if request.method == "GET":
        return render_template("develop/games/upload-version.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj)
    else:
        with limiter.limit("5/minute"):
            AssetFile = request.files.get("file", default = None)
            if AssetFile is None:
                flash("No file was provided", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
            CreateLockName = f"UploadAssetVersion:{str(PlaceAssetObj.id)}"
            CreateLock = redislock.acquire_lock(CreateLockName, acquire_timeout=20, lock_timeout=5)
            
            AssetFile.seek(0)
            AssetFileContent = AssetFile.read()
            AssetFileHash = hashlib.sha512(AssetFileContent).hexdigest()

            CurrentAssetVersion : AssetVersion = GetLatestAssetVersion( PlaceAssetObj )
            if CurrentAssetVersion is not None:
                if CurrentAssetVersion.content_hash == AssetFileHash:
                    redislock.release_lock(CreateLockName, CreateLock)
                    flash("File is the same as the current version", "error")
                    return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
                
            PlaceObj : Place = Place.query.filter_by(placeid=PlaceAssetObj.id).first()

            isValidPlaceFile = ValidatePlaceFile( AssetFile, keepFileWhenInvalid=False, TestPlaceYear = PlaceObj.placeyear ) # if it returns a bool its valid, if it returns a string its invalid and the string is the error
            if type(isValidPlaceFile) == str:
                redislock.release_lock(CreateLockName, CreateLock)
                flash(f"Validation Failed: {isValidPlaceFile}", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
            
            if not s3helper.DoesKeyExist(AssetFileHash):
                s3helper.UploadBytesToS3(AssetFileContent, AssetFileHash)
            
            NewAssetVersion : AssetVersion = CreateNewAssetVersion( PlaceAssetObj, AssetFileHash, ForceNewVersion = True, UploadedBy = AuthenticatedUser)
            if NewAssetVersion is None:
                redislock.release_lock(CreateLockName, CreateLock)
                flash("Failed to create a new asset version", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
            PlaceAssetObj.updated_at = datetime.utcnow()
            UniverseObj.updated_at = datetime.utcnow()
            db.session.commit()

            redislock.release_lock(CreateLockName, CreateLock)
            flash("Successfully updated place file", "success")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/upload-version")
    
@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/placeicon", methods=["GET","POST"])
@auth.authenticated_required
def ManageUniversePlaceIconPage( universeid : int, placeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
    if PlaceObj is None:
        abort(404)
    PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj

    if request.method == "GET":
        return render_template("develop/games/upload-icon.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj, RandomNumber=random.randint(0, 10000000))
    else:
        with limiter.limit("5/minute"):
            useGenerated = request.form.get("usegenerated") == "on"
            if useGenerated:
                PlaceAssetObj.updated_at = datetime.utcnow()
                CurrentPlaceIcon : PlaceIcon = PlaceIcon.query.filter_by(placeid=PlaceAssetObj.id).first()
                if CurrentPlaceIcon is not None:
                    db.session.delete(CurrentPlaceIcon)
                db.session.commit()
                TakeThumbnail( AssetId=PlaceAssetObj.id, isIcon=True )
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
            
            AssetFile = request.files.get("file", default = None)
            if AssetFile is None:
                flash("No file was provided", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
            if AssetFile.content_length > 2097152:
                flash("File size is too big", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
            IconImage = ValidateClothingImage( AssetFile, verifyResolution=False, validateFileSize=False, returnImage=True )
            if IconImage is False or IconImage is None:
                flash("Invalid image", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
            if IconImage.width != IconImage.height:
                flash("Image is not square", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
            if IconImage.width < 256 or IconImage.width > 1024:
                flash("Image is not between 256x256 and 1024x1024", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")
            
            AssetFile = BytesIO()
            IconImage.save(AssetFile, format="PNG")

            AssetFile.seek(0)
            IconImageHash = hashlib.sha512(AssetFile.read()).hexdigest()
            if not s3helper.DoesKeyExist(IconImageHash):
                AssetFile.seek(0)
                s3helper.UploadBytesToS3(AssetFile.read(), IconImageHash, contentType="image/png")
            
            CurrentPlaceIcon : PlaceIcon = PlaceIcon.query.filter_by(placeid=PlaceAssetObj.id).first()
            if CurrentPlaceIcon is None:
                CurrentPlaceIcon = PlaceIcon(placeid=PlaceAssetObj.id, contenthash=IconImageHash, updated_at=datetime.utcnow(), moderation_status=1)
                db.session.add(CurrentPlaceIcon)
            else:
                CurrentPlaceIcon.contenthash = IconImageHash
                CurrentPlaceIcon.updated_at = datetime.utcnow()
                CurrentPlaceIcon.moderation_status = 1
            PlaceAssetObj.updated_at = datetime.utcnow()
            UniverseObj.updated_at = datetime.utcnow()
            db.session.commit()

            flash("Successfully updated place icon", "success")
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/placeicon")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/thumbnails", methods=["GET","POST"])
@auth.authenticated_required
def ManageUniversePlaceThumbnailsPage( universeid : int, placeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
    if PlaceObj is None:
        abort(404)
    PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj

    if request.method == "GET":
        return render_template("develop/games/upload-thumbnail.html", UniverseObj=UniverseObj, RootPlaceAssetObj=RootPlaceAssetObj, PlaceObj=PlaceObj, PlaceAssetObj=PlaceAssetObj, RandomNumber=random.randint(0, 10000000))
    else:
        with limiter.limit("5/minute"):
            useGenerated = request.form.get("usegenerated") == "on"
            if useGenerated:
                PlaceAssetObj.updated_at = datetime.utcnow()
                CurrentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=PlaceAssetObj.id).order_by(AssetThumbnail.asset_version_id.desc()).first()
                if CurrentAssetThumbnail is not None:
                    db.session.delete(CurrentAssetThumbnail)
                db.session.commit()
                TakeThumbnail( AssetId=PlaceAssetObj.id, isIcon=False )
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            
            AssetFile = request.files.get("file", default = None)
            if AssetFile is None:
                flash("No file was provided", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            
            if AssetFile.content_length > 2097152:
                flash("File size is too big", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            ThumbnailImage = ValidateClothingImage( AssetFile, verifyResolution=False, validateFileSize=False, returnImage=True )
            if ThumbnailImage is False or ThumbnailImage is None:
                flash("Invalid image", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            
            if ThumbnailImage.width / ThumbnailImage.height != 16 / 9:
                flash("Image is not 16:9 aspect ratio", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            
            if ThumbnailImage.width < 640 or ThumbnailImage.width > 1920:
                flash("Image is not between 640x360 and 1920x1080", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            if ThumbnailImage.height < 360 or ThumbnailImage.height > 1080:
                flash("Image is not between 640x360 and 1920x1080", "error")
                return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")
            
            AssetFile = BytesIO()
            ThumbnailImage.save(AssetFile, format="PNG")

            AssetFile.seek(0)
            ThumbnailImageHash = hashlib.sha512(AssetFile.read()).hexdigest()
            if not s3helper.DoesKeyExist(ThumbnailImageHash):
                AssetFile.seek(0)
                s3helper.UploadBytesToS3(AssetFile.read(), ThumbnailImageHash, contentType="image/png")
            
            CurrentAssetThumbnail : AssetThumbnail = AssetThumbnail.query.filter_by(asset_id=PlaceAssetObj.id).order_by(AssetThumbnail.asset_version_id.desc()).first()
            LatestAssetVersion : AssetVersion = GetLatestAssetVersion(PlaceAssetObj)
            if LatestAssetVersion is None:
                flash("No asset version found", "error")
                return redirect(f"/develop/{str(PlaceAssetObj.id)}/thumbnails")
            if CurrentAssetThumbnail is None:
                CurrentAssetThumbnail = AssetThumbnail(asset_id=PlaceAssetObj.id, asset_version_id=LatestAssetVersion.version, content_hash=ThumbnailImageHash, created_at=datetime.utcnow(), moderation_status=1)
                db.session.add(CurrentAssetThumbnail)
            else:
                CurrentAssetThumbnail.content_hash = ThumbnailImageHash
                CurrentAssetThumbnail.created_at = datetime.utcnow()
                CurrentAssetThumbnail.moderation_status = 1
                CurrentAssetThumbnail.asset_version_id = LatestAssetVersion.version
            PlaceAssetObj.updated_at = datetime.utcnow()
            UniverseObj.updated_at = datetime.utcnow()
            db.session.commit()
            return redirect(f"/develop/universes/{UniverseObj.id}/place/{PlaceObj.placeid}/thumbnails")

@DevelopPagesRoute.route("/develop/universes/<int:universeid>/place/<int:placeid>/version-history", methods=["GET"])
@auth.authenticated_required
def ManageUniversePlaceVersionHistoryPage( universeid : int, placeid : int ):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UniverseObj : Universe = Universe.query.filter_by(id=universeid).first()
    isUserAllowedtoViewPage(AuthenticatedUser, UniverseObj, abortOnFail=True)
    RootPlaceAssetObj : Asset = Asset.query.filter_by(id = UniverseObj.root_place_id).first()
    PlaceObj : Place = Place.query.filter_by(placeid = placeid, parent_universe_id = UniverseObj.id ).first()
    if PlaceObj is None:
        abort(404)
    PlaceAssetObj : Asset = Asset.query.filter_by(id = PlaceObj.placeid).first() if placeid != UniverseObj.root_place_id else RootPlaceAssetObj

    PageNumber : int = max( 1, request.args.get("page", default = 1, type = int) )
    AssetVersions : list[AssetVersion] = AssetVersion.query.filter_by(asset_id=PlaceAssetObj.id).order_by(AssetVersion.version.desc()).paginate( page = PageNumber, per_page = 10, error_out = False )
    return render_template(
        "/develop/games/version-history.html",
        UniverseObj = UniverseObj,
        PlaceAssetObj = PlaceAssetObj,
        AssetVersions = AssetVersions,
        CDN_URL = config.CDN_URL
    )

@DevelopPagesRoute.route("/develop/<int:assetid>/edit", methods=["GET"])
@auth.authenticated_required
def EditItemPage( assetid : int ):
    AuthenticatedUser = auth.GetCurrentUser()
    AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        abort(404)
    if AssetObj.asset_type not in [AssetType.Shirt, AssetType.TShirt, AssetType.Pants, AssetType.Audio, AssetType.Image]:
        abort(404)
    isUserAllowedtoViewPage(AuthenticatedUser, AssetObj, abortOnFail=True, isGameContext=False)
    return render_template("develop/edit.html", AssetObj=AssetObj)

@DevelopPagesRoute.route("/develop/<int:assetid>/edit", methods=["POST"])
@auth.authenticated_required
@limiter.limit("5/minute")
def EditItem( assetid : int ):
    AuthenticatedUser = auth.GetCurrentUser()
    AssetObj : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObj is None:
        abort(404)
    if AssetObj.asset_type not in [AssetType.Shirt, AssetType.TShirt, AssetType.Pants, AssetType.Audio, AssetType.Image]:
        abort(404)
    isUserAllowedtoViewPage(AuthenticatedUser, AssetObj, abortOnFail=True, isGameContext=False)

    if not websiteFeatures.GetWebsiteFeature("AssetEditing"):
        flash("Asset editing is currently disabled", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")

    AssetName = request.form.get("item-name", default = "", type = str)
    AssetDescription = request.form.get("item-description", default = "", type = str)
    isForSale = request.form.get("is-for-sale", default = "off") == "on"
    AssetRobuxPrice = request.form.get("robux-cost", default = 0, type = int)
    AssetTixPrice = request.form.get("tix-cost", default = 0, type = int)

    if AssetName == "":
        flash("Item name cannot be empty", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    if len(AssetName) > 35:
        flash("Item name cannot be longer than 35 characters", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    if len(AssetDescription) > 200:
        flash("Item description cannot be longer than 200 characters", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    if AssetRobuxPrice < 0 or AssetRobuxPrice > 1000000:
        flash("Robux price must be between 0 and 1,000,000", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    if AssetTixPrice < 0 or AssetTixPrice > 10000000:
        flash("Tix price must be between 0 and 10,000,000", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    if isForSale and AssetObj.moderation_status != 0:
        flash("You cannot sell an item that is not approved yet", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    if isForSale and (AssetObj.asset_type == AssetType.Audio or AssetObj.asset_type == AssetType.Image):
        flash("You cannot sell this type of asset", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    
    UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
    if UserCurrentMembership == MembershipType.NonBuildersClub and isForSale:
        flash("You must be Builders Club to sell items", "error")
        return redirect(f"/develop/{str(AssetObj.id)}/edit")
    
    AssetName = FilterText(AssetName)
    AssetDescription = FilterText(AssetDescription)

    AssetObj.name = AssetName
    AssetObj.description = AssetDescription
    AssetObj.is_for_sale = isForSale
    AssetObj.price_robux = AssetRobuxPrice
    AssetObj.price_tix = AssetTixPrice
    AssetObj.updated_at = datetime.utcnow()
    db.session.commit()
    return redirect(f"/develop/{str(AssetObj.id)}/edit")

def ShutdownServer(JobId):
    from app.routes.jobreporthandler import HandleUserTimePlayed
    from app.services.gameserver_comm import perform_post
    TargetPlaceServer : PlaceServer | None = PlaceServer.query.filter_by(serveruuid=JobId).first()
    if TargetPlaceServer is None:
        return
    MasterServer : GameServer | None = GameServer.query.filter_by(serverId=TargetPlaceServer.originServerId).first()
    if MasterServer is None:
        return
    PlaceObj : Place = Place.query.filter_by(placeid=TargetPlaceServer.serverPlaceId).first()
    UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
    logging.info(f"CloseJob : ShutdownServer func : Closing job {str(JobId)} for place {str(PlaceObj.placeid)}")
    try:
        CloseJobRequest = perform_post(
            TargetGameserver = MasterServer,
            Endpoint = "CloseJob",
            JSONData = {
                "jobid": str(JobId)
            }
        )
        PlaceServerPlayersList : list[PlaceServerPlayer] = PlaceServerPlayer.query.filter_by(serveruuid=JobId).all()
        for player in PlaceServerPlayersList:
            TotalTimePlayed = (datetime.utcnow() - player.joinTime).total_seconds()
            HandleUserTimePlayed(player.user, TotalTimePlayed, serverUUID=str(JobId), placeId=PlaceObj.placeid)
            db.session.delete(player)
        db.session.delete(TargetPlaceServer)
        db.session.commit()

        ClearPlayingCountCache( PlaceObj = PlaceObj )
        ClearUniversePlayingCountCache( UniverseObj = UniverseObj )
    except Exception as e:
        logging.error(f"CloseJob : ShutdownServer func : Failed to close job {str(JobId)} for place {str(PlaceObj.placeid)} : {str(e)}")

    return
