from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
from app.util import auth, turnstile, websiteFeatures
from app.util.embeds import RenderAssetEmbed
from app.routes.thumbnailer import TakeUserThumbnail
from app.util.redislock import acquire_lock, release_lock
from app.extensions import db, csrf, limiter, redis_controller, user_limiter
from app.models.asset import Asset
from app.models.usereconomy import UserEconomy
from app.models.userassets import UserAsset
from app.models.user_avatar_asset import UserAvatarAsset
from app.models.user import User
from app.models.package_asset import PackageAsset
from app.models.gamepass_link import GamepassLink
from app.models.asset_rap import AssetRap
from app.models.groups import Group
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.universe import Universe
from app.models.limited_item_transfers import LimitedItemTransfer
from app.routes.badgesapi import CalculateBadgeRarity, GetBadgeAwardedPastDay
from app.pages.catalog.catalogtypes import CatalogTypes
from app.pages.messages.messages import CreateSystemMessage
from slugify import slugify
from datetime import datetime
from config import Config
config = Config()
from sqlalchemy import text, and_, or_
import math
from app.enums.AssetType import AssetType
from app.util.transactions import CreateTransactionForSale
import logging
import calendar
import time
import requests
import redis_lock
from app.util.membership import GetUserMembership
from app.enums.MembershipType import MembershipType
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
from app.models.user_ban import UserBan
from app.enums.BanType import BanType
from app.services.economy import IncrementTargetBalance, AdjustAssetRap, GetAssetRap, TaxCurrencyAmount, GetUserBalance, GetGroupBalance, DecrementTargetBalance,EconomyLockAcquireException, InsufficientFundsException
from app.services.groups import GetUserFromId, GetGroupFromId

CatalogRoute = Blueprint('catalog', __name__, template_folder="pages", url_prefix="/catalog")
LibraryRoute = Blueprint('library', __name__, template_folder="pages", url_prefix="/library")
BadgesPageRoute = Blueprint('badges_page', __name__, template_folder="pages", url_prefix="/badges")

def IncrementAssetCreator( AssetObject : Asset, AmountGiven : int, CurrencyType : int):
    """ Increments the robux of the asset creator"""
    CurrencyToGive = math.floor(AmountGiven * 0.7)
    if CurrencyToGive <= 0:
        return True
    
    if AssetObject.creator_type == 0:
        # User
        IncrementTargetBalance(GetUserFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
        return True
    
    elif AssetObject.creator_type == 1:
        # Group
        IncrementTargetBalance(GetGroupFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
        return True

def ConvertQueryToAsset( queryResult ):
    """Converts a query result to a dict with the same keys as the Asset model"""
    return {
        "id": queryResult[0],
        "roblox_asset_id": queryResult[1],
        "name": queryResult[2],
        "description": queryResult[3],
        "created_at": queryResult[4],
        "updated_at": queryResult[5],
        "asset_type": queryResult[6],
        "asset_genre": queryResult[7],
        "creator_type": queryResult[8],
        "creator_id": queryResult[9],
        "moderation_status": queryResult[10],
        "is_for_sale": queryResult[11],
        "price_robux": queryResult[12],
        "price_tix": queryResult[13],
        "is_limited": queryResult[14],
        "is_limited_unique": queryResult[15],
        "serial_count": queryResult[16],
        "sale_count": queryResult[17],
        "offsale_at": queryResult[18]
    }

OrderTypes = {
    0: "created_at DESC", # Default
    1: "price_robux ASC", # Lowest to Highest
    2: "price_robux DESC", # Highest to Lowest
    3: "updated_at DESC", # Recently Updated
    4: "sale_count DESC" # Best Selling
}

import urllib.parse

@CatalogRoute.route("/", methods=["POST"])
@auth.authenticated_required
@csrf.exempt
def catalog_search():
    CategoryType = int(request.args.get(key="category", default=0, type=int))
    OrderBy = request.form.get(key="order-by", default=0, type=int)
    if OrderBy not in OrderTypes:
        OrderBy = 0
    SearchQuery = request.form.get(key="search-input", default="", type=str)
    if len(SearchQuery) < 3:
        SearchQuery = ""
    if SearchQuery == "":
        return redirect(f"/catalog/?sort={OrderBy}&category={str(CategoryType)}")
    # Make sure the query is url safe
    SearchQuery = urllib.parse.quote(SearchQuery)

    return redirect(f"/catalog/?q={SearchQuery}&sort={OrderBy}&category={str(CategoryType)}")

@CatalogRoute.route("/", methods=["GET"])
@auth.authenticated_required
def catalog_page():
    CatalogType = int(request.args.get("category", 0))
    if CatalogType not in CatalogTypes:
        return redirect("/catalog/")
    if request.args.get("page") is None:
        Page = 1
    else:
        try:
            Page = int(request.args.get("page"))
        except:
            Page = 1
    SearchInput = request.args.get("q", "", type=str)
    """
        Types of sorting:
        0 - Relevance (Default) [ Just sort by created_at ]
        1 - Price (Lowest to Highest)
        2 - Price (Highest to Lowest)
        3 - Recently Updated
        4 - Best Selling
    """
    if request.args.get("sort") is None:
        SortType = 0
    else:
        try:
            SortType = int(request.args.get("sort"))
        except:
            SortType = 0
    if SortType > 4 or SortType < 0:
        SortType = 0
    SearchQuery = Asset.query
    CatalogTypesDict = {
        0: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
            AssetType.Hat,
            AssetType.TShirt,
            AssetType.Shirt,
            AssetType.Pants,
            AssetType.Gear,
            AssetType.Face,
            AssetType.Head,
            AssetType.HairAccessory,
            AssetType.FaceAccessory,
            AssetType.NeckAccessory,
            AssetType.ShoulderAccessory,
            AssetType.FrontAccessory,
            AssetType.BackAccessory,
            AssetType.WaistAccessory,
            AssetType.Package
        ))).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0)).filter(or_(
            Asset.is_for_sale == True, Asset.is_limited == True
        )),
        1: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
            AssetType.Hat,
            AssetType.HairAccessory,
            AssetType.FaceAccessory,
            AssetType.NeckAccessory,
            AssetType.ShoulderAccessory,
            AssetType.FrontAccessory,
            AssetType.BackAccessory,
            AssetType.WaistAccessory,
        ))).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0)).filter(or_(
            Asset.is_for_sale == True, Asset.is_limited == True
        )),
        2: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0, Asset.is_for_sale == True)),
        3: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0, Asset.is_for_sale == True)),
        4: lambda queryObj: queryObj.filter_by(is_limited = True),
        5: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
            AssetType.Hat,
            AssetType.HairAccessory,
            AssetType.FaceAccessory,
            AssetType.NeckAccessory,
            AssetType.ShoulderAccessory,
            AssetType.FrontAccessory,
            AssetType.BackAccessory,
            AssetType.WaistAccessory,
        ))).filter_by(is_limited = True),
        6: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter_by(is_limited = True),
        7: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_limited = True),
        8: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
            AssetType.Hat,
            AssetType.TShirt,
            AssetType.Shirt,
            AssetType.Pants,
            AssetType.Package,
        ))).filter_by(is_for_sale = True),
        9: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
            AssetType.Hat,
            AssetType.HairAccessory,
            AssetType.FaceAccessory,
            AssetType.NeckAccessory,
            AssetType.ShoulderAccessory,
            AssetType.FrontAccessory,
            AssetType.BackAccessory,
            AssetType.WaistAccessory,
        ))).filter_by(is_for_sale = True),
        10: lambda queryObj: queryObj.filter_by(asset_type = AssetType.Shirt).filter_by(is_for_sale = True),
        11: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.TShirt).filter_by(is_for_sale = True),
        12: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Pants).filter_by(is_for_sale = True),
        13: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Package).filter_by(is_for_sale = True),
        14: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
            AssetType.Head,
            AssetType.Face,
            AssetType.Package
        ))).filter_by(is_for_sale = True),
        15: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Head).filter_by(is_for_sale = True),
        16: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_for_sale = True),
        41: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.HairAccessory).filter_by(is_for_sale = True),
    }
    if CatalogType > 41 or CatalogType < 0:
        CatalogType = 0
    SearchQuery = CatalogTypesDict[CatalogType](SearchQuery)
    if SearchInput != "" and len(SearchInput) > 3:        
        SearchQuery = SearchQuery.filter( Asset.name.ilike(f"%{SearchInput}%") )
    else:
        SearchInput = ""
    if SortType == 0:
        SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
    elif SortType == 1:
        SearchQuery = SearchQuery.order_by(Asset.price_robux.asc())
    elif SortType == 2:
        SearchQuery = SearchQuery.order_by(Asset.price_robux.desc())
    elif SortType == 3:
        SearchQuery = SearchQuery.order_by(Asset.updated_at.desc())
    elif SortType == 4:
        SearchQuery = SearchQuery.order_by(Asset.sale_count.desc())
    else:
        SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
    SearchQuery = SearchQuery.paginate(
        page=Page,
        per_page=24,
        error_out=False
    )
    for AssetObj in SearchQuery.items:
        if AssetObj.is_limited and not AssetObj.is_for_sale:
            BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=AssetObj.id, is_for_sale=True).order_by(UserAsset.price.asc()).first()
            if BestPriceResult is not None:
                AssetObj.best_price = str(BestPriceResult.price)
            else:
                AssetObj.best_price = "--"
    if Page == 1:
        PreviousPage = -1
    else:
        PreviousPage = Page -1
    if SearchQuery.has_next:
        NextPage = Page + 1
    else:
        NextPage = -1
    
    return render_template("catalog/index.html", 
                           categoryname=CatalogTypes[CatalogType]["name"], 
                           queryResults = SearchQuery.items, 
                           categoryid=CatalogType,
                            PreviousPage=PreviousPage,
                            NextPage=NextPage,
                            PageNumber=Page,
                            CatalogType=CatalogType,
                            SortType=SortType,
                            query=SearchInput,
                            totalPages = SearchQuery.pages,
                            totalResults = SearchQuery.total)

@CatalogRoute.route("/<int:assetid>/", methods=["GET"])
@CatalogRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
@auth.authenticated_or_bot_required
def asset_page(assetid, assetname=None):
    AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObject is None:
        return redirect("/catalog")
    SlugName = slugify(AssetObject.name, lowercase=False)
    if SlugName is None or SlugName == "":
        SlugName = "unnamed"
    if assetname is None:
        if request.args.get("page") is None:
            return redirect(f"/catalog/{assetid}/{SlugName}")
        else:
            return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
    if assetname != SlugName:
        if request.args.get("page") is None:
            return redirect(f"/catalog/{assetid}/{SlugName}")
        else:
            return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
    if AssetObject.asset_type.value not in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
        if AssetObject.asset_type.value == 9:
            return redirect(f"/games/{assetid}/")
        if AssetObject.asset_type.value in [1,3,4,5,10,13,24,34,38,40]:
            return redirect(f"/library/{assetid}/")
        return redirect("/catalog/")
    if auth.IsCrawlerBot():
        return RenderAssetEmbed(
            title=AssetObject.name,
            description=AssetObject.description,
            image_path=f"/Thumbs/Asset.ashx?assetId={AssetObject.id}&x=420&y=420",
            canonical_path=f"/catalog/{AssetObject.id}/{SlugName}"
        )
    CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
    Created = AssetObject.created_at.strftime("%d/%m/%Y")
    Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
    AuthenticatedUser : User = auth.GetCurrentUser()
    doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
    BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
    BestPrice = "None"
    if BestPriceResult is not None:
        BestPrice = str(BestPriceResult.price)
    UserOwns = 0
    PrivateSaleList = []
    NextPage = 0
    PreviousPage = 0
    PageNumber = 0
    AssetRap = 0
    if AssetObject.is_limited and not AssetObject.is_for_sale:
        AssetRap = GetAssetRap(assetid)
        UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
        Page = 1
        if request.args.get("page"):
            try:
                Page = int(request.args.get("page"))
            except:
                pass
        PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5)
        for sale in PrivateSales.items:
            SellerUser : User = User.query.filter_by(id=sale.userid).first()
            PrivateSaleList.append({
                "price": sale.price,
                "seller": SellerUser.username,
                "sellerid": SellerUser.id,
                "serial": sale.serial,
                "uaid": sale.id
            })
        if PrivateSales.has_next:
            NextPage = Page + 1
        else:
            NextPage = -1
        if Page > 1:
            PreviousPage = Page - 1
        else:
            PreviousPage = -1
        PageNumber = Page
    OffsaleAt = None
    if AssetObject.offsale_at is not None:
        if datetime.utcnow() > AssetObject.offsale_at:
            AssetObject.is_for_sale = False
            AssetObject.offsale_at = None
            db.session.commit()
        else:
            OffsaleAt = int(calendar.timegm(AssetObject.offsale_at.timetuple()))
    return render_template("catalog/asset.html", asset=AssetObject, creator=CreatorObj, createddate=Created, 
                           updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice, 
                           BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
                           NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
                           OffsaleAt=OffsaleAt)

@CatalogRoute.route("/resell/<int:assetid>", methods=["GET"])
@auth.authenticated_required
def resell_page(assetid):
    AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObject is None:
        return redirect("/catalog")
    if not AssetObject.is_limited:
        return redirect(f"/catalog/{str(AssetObject.id)}/")
    if AssetObject.is_for_sale:
        return redirect(f"/catalog/{str(AssetObject.id)}/") # Users cant resell items when it is still for sale
    AuthenticatedUser : User = auth.GetCurrentUser()
    UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
    if UserOwns == 0:
        return redirect(f"/catalog/{str(AssetObject.id)}/")
    AllUserAssets = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).all()
    UserAssetCurrentlyForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=True).count()
    UserAssetNotForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=False).count()
    return render_template("catalog/resell.html", asset=AssetObject, userassets=AllUserAssets, userassetforsalecount=UserAssetCurrentlyForSale, userassetnotforsalecount=UserAssetNotForSale, isOTPRequired = AuthenticatedUser.TOTPEnabled)

@CatalogRoute.route("/resell/<int:assetid>", methods=["POST"])
@auth.authenticated_required
@user_limiter.limit("1/second")
def resell_page_post(assetid):
    AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObject is None:
        return redirect("/catalog")
    if not AssetObject.is_limited:
        return redirect(f"/catalog/{str(AssetObject.id)}/")
    if AssetObject.is_for_sale:
        return redirect(f"/catalog/{str(AssetObject.id)}/")
    AuthenticatedUser : User = auth.GetCurrentUser()
    if "uaid" not in request.form or "itemprice" not in request.form:
        flash("Invalid request", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    try:
        ItemPrice = int(request.form["itemprice"])
        UAID = int(request.form["uaid"])
    except:
        flash("Invalid request", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    UserAssetObj : UserAsset = UserAsset.query.filter_by(id=UAID).first()
    if UserAssetObj is None:
        flash("Invalid request", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    if UserAssetObj.userid != AuthenticatedUser.id:
        flash("Invalid request", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    if UserAssetObj.assetid != AssetObject.id:
        flash("Invalid request", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")

    if not websiteFeatures.GetWebsiteFeature("ItemReselling"):
        flash("Item reselling is currently disabled", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")

    if AuthenticatedUser.TOTPEnabled:
        if "2fa-code" not in request.form:
            flash("Invalid request", "error")
            return redirect(f"/catalog/resell/{str(AssetObject.id)}")
        TOTPCode = request.form["2fa-code"]
        if len(TOTPCode) != 6:
            flash("Invalid 2FA code", "error")
            return redirect(f"/catalog/resell/{str(AssetObject.id)}")
        if not auth.Validate2FACode(AuthenticatedUser.id, TOTPCode):
            flash("Invalid 2FA code", "error")
            return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    if ItemPrice < 1 or ItemPrice > 999999999:
        flash("Invalid price ( 1 - 999999999 )", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    if ItemPrice <= 49 or ItemPrice > 3000:
        ExistingWarning : UserBan = UserBan.query.filter_by(userid=AuthenticatedUser.id, acknowledged=False).order_by(UserBan.id.desc()).first()
        if ExistingWarning is None:
            AutoWarning = UserBan(
                userid=AuthenticatedUser.id,
                author_userid=1,
                ban_type=BanType.Warning,
                reason="Don't list an item for under 50 or over 3000 Robux.",
                moderator_note=f"Auto ban, listed {AssetObject.id} ({AssetObject.name}) for {ItemPrice} Robux.",
                expires_at=None
            )
            db.session.add(AutoWarning)
            AuthenticatedUser.accountstatus = 2
            db.session.commit()
            try:
                requests.post(
                    url=config.DISCORD_ADMIN_LOGS_WEBHOOK,
                    json={"content": f"**[{AuthenticatedUser.username}]({config.BaseURL}/admin/manage-users/{AuthenticatedUser.id})** has been autobanned for selling {AssetObject.name} for {ItemPrice} Robux."},
                    timeout=10
                )
            except Exception as e:
                logging.warning(f"resell automod webhook failed: {e}")
        flash("Your listing was removed.", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    if UserAssetObj.is_for_sale:
        flash("This asset is already for sale", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
    if UserCurrentMembership == MembershipType.NonBuildersClub:
        flash("You must be a Builders Club member to sell items", "error")
        return redirect(f"/catalog/resell/{str(AssetObject.id)}")
    UserAssetObj.is_for_sale = True
    UserAssetObj.price = ItemPrice
    db.session.commit()
    return redirect(f"/catalog/resell/{str(AssetObject.id)}")

@CatalogRoute.route("/resell/<int:uaid>/takeoff", methods=["POST"])
@auth.authenticated_required
@csrf.exempt
def resell_takeoff(uaid):
    AuthenticatedUser : User = auth.GetCurrentUser()
    UserAssetObject : UserAsset = UserAsset.query.filter_by(id=uaid).first()
    if UserAssetObject is None:
        flash("Invalid asset", "error")
        return redirect("/catalog")
    if UserAssetObject.userid != AuthenticatedUser.id:
        flash("Invalid asset", "error")
        return redirect("/catalog")
    if not UserAssetObject.is_for_sale:
        flash("This asset is not for sale", "error")
        return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
    UserAssetObject.is_for_sale = False
    UserAssetObject.price = 0
    db.session.commit()
    return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")

@CatalogRoute.route("/api/purchase", methods=["POST"])
@auth.authenticated_required_api
@limiter.limit("1/second")
@user_limiter.limit("1/second")
def api_purchase():
    JSONData = request.json
    if "assetId" not in JSONData or "expectedPrice" not in JSONData or "currencyType" not in JSONData:
        return jsonify({"success": False, "message": "Invalid request"}),400

    if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
        return jsonify({"success": False, "message": "Purchasing is temporarily disabled"}),400

    try:
        JSONData["assetId"] = int(JSONData["assetId"])
        JSONData["expectedPrice"] = int(JSONData["expectedPrice"])
        JSONData["currencyType"] = int(JSONData["currencyType"])
    except:
        return jsonify({"success": False, "message": "Invalid request"}),400

    AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
    if AssetObject is None:
        return jsonify({"success": False, "message": "Invalid asset"}),400

    AuthenticatedUser : User = auth.GetCurrentUser()
    
    try:
        with redis_lock.Lock(redis_client = redis_controller, name=f"asset:{str(AssetObject.id)}", expire=15, auto_renewal=True, strict=True) as lock:
            if AssetObject.offsale_at is not None:
                if datetime.utcnow() > AssetObject.offsale_at:
                    AssetObject.is_for_sale = False
                    AssetObject.offsale_at = None
                    db.session.commit()
            if AssetObject.is_for_sale == False:
                return jsonify({"success": False, "message": "Asset is not for sale"}),400
            if JSONData["currencyType"] == 0:
                if JSONData["expectedPrice"] != AssetObject.price_robux:
                    return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
            elif JSONData["currencyType"] == 1:
                if JSONData["expectedPrice"] != AssetObject.price_tix:
                    return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
            else:
                return jsonify({"success": False, "message": "Invalid currency type"}),400

            if AssetObject.price_robux == 0 and AssetObject.price_tix != 0 and JSONData["currencyType"] == 0:
                return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
            if AssetObject.price_tix == 0 and AssetObject.price_robux != 0 and JSONData["currencyType"] == 1:
                return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
            
            UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=AssetObject.id).first()
            if UserAssetObj is not None:
                return jsonify({"success": False, "message": "User already owns asset"}),400
            PurchaserRobuxBal, PurchaserTicketsBal = GetUserBalance(AuthenticatedUser)
            if JSONData["currencyType"] == 0:
                if PurchaserRobuxBal < AssetObject.price_robux:
                    return jsonify({"success": False, "message": "Insufficient funds"}),400
                try:
                    DecrementTargetBalance(
                        Target = AuthenticatedUser,
                        Amount = AssetObject.price_robux,
                        CurrencyType = 0
                    )
                except InsufficientFundsException:
                    return jsonify({"success": False, "message": "Insufficient funds"}),400
                except EconomyLockAcquireException:
                    return jsonify({"success": False, "message": "Failed to acquire lock"}),400
                except Exception as e:
                    logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
                    return jsonify({"success": False, "message": "Failed to decrement balance"}),400
                
                IncrementAssetCreator(
                    AssetObject = AssetObject,
                    AmountGiven = AssetObject.price_robux,
                    CurrencyType = 0
                )
            elif JSONData["currencyType"] == 1:
                if PurchaserTicketsBal < AssetObject.price_tix:
                    return jsonify({"success": False, "message": "Insufficient funds"}),400
                
                try:
                    DecrementTargetBalance(
                        Target = AuthenticatedUser,
                        Amount = AssetObject.price_tix,
                        CurrencyType = 1
                    )
                except InsufficientFundsException:
                    return jsonify({"success": False, "message": "Insufficient funds"}),400
                except EconomyLockAcquireException:
                    return jsonify({"success": False, "message": "Failed to acquire lock"}),400
                except Exception as e:
                    logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
                    return jsonify({"success": False, "message": "Failed to decrement balance"}),400
                
                IncrementAssetCreator(
                    AssetObject = AssetObject,
                    AmountGiven = AssetObject.price_tix,
                    CurrencyType = 1
                )
            else:
                return jsonify({"success": False, "message": "Invalid currency type"}),400
            
            if AssetObject.is_limited_unique:
                ItemSerial = AssetObject.sale_count + 1
                if AssetObject.sale_count + 1 >= AssetObject.serial_count:
                    AssetObject.is_for_sale = False
            if AssetObject.is_limited_unique:
                UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id, serial=ItemSerial)
            else:
                UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id)

            if AssetObject.asset_type == AssetType.Package:
                PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObject.id).all()
                for PackageAssetObj in PackageAssets:
                    NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id)
                    db.session.add(NewPackageAssetObj)

            AssetObject.sale_count += 1
            db.session.add(UserAssetObj)
            db.session.commit()

            try:
                if AssetObject.creator_type == 0:
                    SellerUserObj : User = User.query.filter_by(id=AssetObject.creator_id).first()
                    CreateTransactionForSale(
                        AssetObj = AssetObject,
                        PurchasePrice = JSONData["expectedPrice"],
                        PurchaseCurrencyType = JSONData["currencyType"],
                        Seller = SellerUserObj,
                        Buyer = AuthenticatedUser,
                        ApplyTaxAutomatically = True
                    )
                else:
                    GroupObj : Group = Group.query.filter_by(id=AssetObject.creator_id).first()
                    CreateTransactionForSale(
                        AssetObj = AssetObject,
                        PurchasePrice = JSONData["expectedPrice"],
                        PurchaseCurrencyType = JSONData["currencyType"],
                        Seller = GroupObj,
                        Buyer = AuthenticatedUser,
                        ApplyTaxAutomatically = True
                    )
            except Exception as e:
                logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
                pass

            return jsonify({"success": True, "message": "Asset purchased successfully"}),200
    except AssertionError as e:
        return jsonify({"success": False, "message": "Failed to acquire lock"}),400

@CatalogRoute.route("/api/purchase-limited", methods=["POST"])
@auth.authenticated_required_api
@limiter.limit("1/second")
@user_limiter.limit("1/second")
def api_purchase_limited():
    JSONData = request.json
    if "assetId" not in JSONData or "expectedPrice" not in JSONData or "expectedOwner" not in JSONData or "itemOwnershipId" not in JSONData:
        return jsonify({"success": False, "message": "Invalid request"}),400
    # Expected Types: assetId (int), expectedPrice (int), expectedOwner (int), itemOwnershipId (int)
    if type(JSONData["assetId"]) != int or type(JSONData["expectedPrice"]) != int or type(JSONData["expectedOwner"]) != int or type(JSONData["itemOwnershipId"]) != int:
        return jsonify({"success": False, "message": "Invalid request"}),400
    AuthenticatedUser : User = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
    EconomyLock = acquire_lock(f"economy:{str(AuthenticatedUser.id)}", acquire_timeout=5, lock_timeout=1)
    if EconomyLock is False:
        return jsonify({"success": False, "message": "Failed to acquire lock"}),400
    AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
    if AssetObject is None:
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": "Invalid asset"}),400
    if AssetObject.is_limited == False:
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": "Asset is not limited"}),400
    LimitedAsset : UserAsset = UserAsset.query.filter_by(assetid=AssetObject.id, id=JSONData["itemOwnershipId"]).first()
    if LimitedAsset is None:
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": "Invalid asset info"}),400
    ItemLock = acquire_lock(f"item:{str(LimitedAsset.id)}", acquire_timeout=5, lock_timeout=1)
    if ItemLock is False:
        return jsonify({"success": False, "message": "Failed to acquire lock ( This is usually caused by too many people purchasing this item at the same time. )"}),400
    if LimitedAsset.is_for_sale == False:
        release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": "Item is not for sale"}),400
    if JSONData["expectedOwner"] != LimitedAsset.userid:
        release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": f"Expected Owner ({str(JSONData['expectedOwner'])}) does not match current item owner({str(LimitedAsset.userid)})"}),400
    if JSONData["expectedPrice"] != LimitedAsset.price:
        release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": f"Expected Price ({str(JSONData['expectedPrice'])}) does not match current item price ({str(LimitedAsset.price)})"}),400
    if AuthenticatedUser.id == LimitedAsset.userid:
        release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
        release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
        return jsonify({"success": False, "message": "You cannot purchase your own item"}),400
    
    UserEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=AuthenticatedUser.id).first()
    if UserEconomyObj.robux < LimitedAsset.price:
        redis_controller.delete(f"economy:{str(AuthenticatedUser.id)}")
        return jsonify({"success": False, "message": "Insufficient funds"}),400
    
    UserEconomyObj.robux -= LimitedAsset.price
    OriginalPrice : int = LimitedAsset.price
    db.session.commit()
    IncrementTargetBalance(GetUserFromId(LimitedAsset.userid), TaxCurrencyAmount(LimitedAsset.price), 0)
    AdjustAssetRap(LimitedAsset.assetid, LimitedAsset.price)
    LimitedAsset.userid = AuthenticatedUser.id
    LimitedAsset.is_for_sale = False
    LimitedAsset.price = 0
    LimitedAsset.updated = datetime.utcnow()
    db.session.commit()
    # We release the locks first as taking a thumbnail can take a while
    release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
    release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
    PreviousOwnerId : int = JSONData["expectedOwner"]
    # Check how many items the previous owner had of the same asset
    PreviousOwnerItems = UserAsset.query.filter_by(userid=PreviousOwnerId, assetid=AssetObject.id).count()
    if PreviousOwnerItems == 0:
        # Remove the asset from the previous owner avatar if there is any
        AvatarAsset = UserAvatarAsset.query.filter_by(user_id=PreviousOwnerId, asset_id=AssetObject.id).first()
        if AvatarAsset is not None:
            db.session.delete(AvatarAsset)
            db.session.commit()
            TakeUserThumbnail(PreviousOwnerId, True, False)
    CreateSystemMessage(
        f"Your {AssetObject.name} has been sold!",
        f"Your item {AssetObject.name} ( UAID: {str(LimitedAsset.id)} / Serial: {str(LimitedAsset.serial)} ) has been sold for R$ {str(OriginalPrice)} to {AuthenticatedUser.username} ( {str(AuthenticatedUser.id)} ).",
        PreviousOwnerId
    )
    SellerUserObj : User = User.query.filter_by(id=PreviousOwnerId).first()
    
    try:
        CreateTransactionForSale(
            AssetObj = AssetObject,
            PurchasePrice = OriginalPrice,
            PurchaseCurrencyType = 0,
            Seller = SellerUserObj,
            Buyer = AuthenticatedUser,
            ApplyTaxAutomatically = True
        )
    except Exception as e:
        logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
        pass

    try:
        newLimitedTransfer = LimitedItemTransfer(
            original_owner_id = SellerUserObj.id,
            new_owner_id = AuthenticatedUser.id,
            asset_id = AssetObject.id,
            user_asset_id = LimitedAsset.id,
            transfer_method = LimitedItemTransferMethod.Purchase,
            purchased_price = OriginalPrice
        )
        db.session.add(newLimitedTransfer)
        db.session.commit()
    except Exception as e:
        logging.warn(f"Failed to create limited item transfer log for asset {str(AssetObject.id)}, message: {str(e)}")
        pass

    return jsonify({"success": True, "message": "Item purchased successfully"}),200


@LibraryRoute.route("/<int:assetid>/", methods=["GET"])
@LibraryRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
@auth.authenticated_or_bot_required
def asset_page(assetid, assetname=None):
    AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
    if AssetObject is None:
        return redirect("/library")
    SlugName = slugify(AssetObject.name, lowercase=False)
    if SlugName is None or SlugName == "":
        SlugName = "unnamed"
    if assetname is None:
        if request.args.get("page") is None:
            return redirect(f"/library/{assetid}/{SlugName}")
        else:
            return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
    if assetname != SlugName:
        if request.args.get("page") is None:
            return redirect(f"/library/{assetid}/{SlugName}")
        else:
            return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
    if AssetObject.asset_type.value in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
        return redirect(f"/catalog/{str(AssetObject.id)}/")
    if AssetObject.asset_type.value == 9:
         return redirect(f"/games/{assetid}/")
    if auth.IsCrawlerBot():
        return RenderAssetEmbed(
            title=AssetObject.name,
            description=AssetObject.description,
            image_path=f"/Thumbs/Asset.ashx?assetId={AssetObject.id}&x=420&y=420",
            canonical_path=f"/library/{AssetObject.id}/{SlugName}"
        )
    CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
    Created = AssetObject.created_at.strftime("%d/%m/%Y")
    Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
    AuthenticatedUser : User = auth.GetCurrentUser()
    doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
    BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
    BestPrice = "None"
    if BestPriceResult is not None:
        BestPrice = str(BestPriceResult.price)
    UserOwns = 0
    PrivateSaleList = []
    NextPage = 0
    PreviousPage = 0
    PageNumber = 0
    AssetRap = 0
    if AssetObject.is_limited and not AssetObject.is_for_sale:
        AssetRap = GetAssetRap(assetid)
        UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
        Page = 1
        if request.args.get("page"):
            try:
                Page = int(request.args.get("page"))
            except:
                pass
        PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5).items
        for sale in PrivateSales:
            SellerUser : User = User.query.filter_by(id=sale.userid).first()
            PrivateSaleList.append({
                "price": sale.price,
                "seller": SellerUser.username,
                "sellerid": SellerUser.id,
                "serial": sale.serial,
                "uaid": sale.id
            })
        if len(UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page+1, per_page=5, error_out=False).items) > 0:
            NextPage = Page + 1
        else:
            NextPage = -1
        if Page > 1:
            PreviousPage = Page - 1
        else:
            PreviousPage = -1
        PageNumber = Page
    
    GamepassLinkObj = None
    GamepassRootPlaceAsset = None
    if AssetObject.asset_type == AssetType.GamePass:
        GamepassLinkObj : GamepassLink = GamepassLink.query.filter_by(gamepass_id=AssetObject.id).first()
        GamepassUniverse : Universe = Universe.query.filter_by(id=GamepassLinkObj.universe_id).first()
        GamepassRootPlaceAsset : Asset = Asset.query.filter_by( id = GamepassUniverse.root_place_id ).first()
    return render_template("catalog/library.html", asset=AssetObject, creator=CreatorObj, createddate=Created, 
                           updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice, 
                           BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
                           NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
                           GamepassLinkObj=GamepassLinkObj, GamepassRootPlaceAsset=GamepassRootPlaceAsset)

@BadgesPageRoute.route("/<int:badgeid>/", methods=["GET"])
@BadgesPageRoute.route("/<int:badgeid>/<badgename>", methods=["GET"])
@auth.authenticated_or_bot_required
def badge_page( badgeid : int, badgename : str = None ):
    BadgeObject : PlaceBadge = PlaceBadge.query.filter_by(id=badgeid).first()
    if BadgeObject is None:
        return abort(404)

    SlugName = slugify(BadgeObject.name, lowercase=False)
    if badgename is None:
        return redirect(f"/badges/{badgeid}/{SlugName}")
    elif badgename != SlugName:
        return redirect(f"/badges/{badgeid}/{SlugName}")
    if auth.IsCrawlerBot():
        return RenderAssetEmbed(
            title=BadgeObject.name,
            description=BadgeObject.description,
            image_path=f"/Thumbs/Asset.ashx?assetId={BadgeObject.icon_image_id}&x=420&y=420",
            canonical_path=f"/badges/{BadgeObject.id}/{SlugName}"
        )

    AuthenticatedUser : User = auth.GetCurrentUser()
    UserBadgeObj : UserBadge = UserBadge.query.filter_by(user_id=AuthenticatedUser.id, badge_id=badgeid).first()
    AssociatedPlaceAssetObj : Asset = Asset.query.filter_by(id=BadgeObject.associated_place_id).first()
    CreatorObj : User | Group = User.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first() if AssociatedPlaceAssetObj.creator_type == 0 else Group.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first()
    Created = BadgeObject.created_at.strftime("%d/%m/%Y")
    Updated = BadgeObject.updated_at.strftime("%d/%m/%Y")

    return render_template(
        "catalog/badges.html", 
        badge = BadgeObject, 
        userbadge = UserBadgeObj, 
        AssociatedPlaceAssetObj = AssociatedPlaceAssetObj, 
        creator = CreatorObj,
        createddate = Created,
        updateddate = Updated
    )
