#badges.roblox.com
import logging
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, abort, jsonify, make_response, after_this_request, Response
from app.util import auth
from app.extensions import db, csrf, limiter, redis_controller
from flask_wtf.csrf import CSRFError, generate_csrf

from app.models.user import User
from app.models.groups import Group
from app.models.place import Place
from app.models.userassets import UserAsset
from app.models.place_badge import PlaceBadge, UserBadge
from app.models.asset import Asset
from app.models.game_session_log import GameSessionLog
from app.models.placeserver_players import PlaceServerPlayer
from app.models.placeservers import PlaceServer
from app.models.universe import Universe

from datetime import datetime, timedelta
from sqlalchemy import and_

BadgesAPIRoute = Blueprint( 'BadgesAPIRoute', __name__, url_prefix='/')

def CalculateBadgeRarity( badge_id : int, bypass_cache : bool = False ) -> float:
    """ 
        Badges have a rarity value that is influenced by the ratio of the number of users in the past 24 hours
        to achieve the badge to the total number of players that have joined the experience within the same timespan; 
        for example, if 99,900 of 100,000 daily visitors receive a badge, its difficulty will likely be Freebie (99.9%), 
        while if only 100 players get a badge in the same experience, its difficulty will appear as Impossible (0.1%).
        
        https://roblox.fandom.com/wiki/Player_badge 
    """

    def _calculate_rarity() -> float:
        BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = badge_id ).first()
        if BadgeObj is None:
            return 0.0
        PlaceObj : Place = Place.query.filter_by( placeid = BadgeObj.associated_place_id ).first()
        UniverseObj : Universe = Universe.query.filter_by( id = PlaceObj.parent_universe_id ).first()
        AwardedRecentlyCount : int = UserBadge.query.filter_by( badge_id = badge_id ).filter( UserBadge.awarded_at > datetime.utcnow() - timedelta( hours = 24 ) ).count()
        TotalPlayedRecentlyCount : int = GameSessionLog.query.filter_by( place_id = UniverseObj.root_place_id ).filter( GameSessionLog.joined_at > datetime.utcnow() - timedelta( hours = 24 ) ).distinct( GameSessionLog.user_id ).count()

        if TotalPlayedRecentlyCount == 0 or AwardedRecentlyCount == 0:
            return 0.0
        
        return round( AwardedRecentlyCount / TotalPlayedRecentlyCount, 3 )

    CacheRedisKey = f"badge_rarity_{badge_id}"

    if not bypass_cache:
        CachedValue = redis_controller.get( CacheRedisKey )
        if CachedValue is not None:
            return float( CachedValue )

    RarityValue = _calculate_rarity()
    redis_controller.set( CacheRedisKey, str(RarityValue), ex = 60 )

    return RarityValue

def GetBadgeAwardedPastDay( badge_id ) -> int:
    timenow = datetime.utcnow()
    start_of_yesterday = datetime(timenow.year, timenow.month, timenow.day) - timedelta(days=1)
    end_of_yesterday = datetime(timenow.year, timenow.month, timenow.day) - timedelta(seconds=1)
    return UserBadge.query.filter_by( badge_id = badge_id ).filter( and_( UserBadge.awarded_at > start_of_yesterday, UserBadge.awarded_at < end_of_yesterday ) ).count()

class UserAlreadyHasBadgeException( Exception ):
    pass

def AwardBadgeToUser( badge_id : int, user_obj : User ) -> UserBadge:
    """ Award a badge to a user. """
    BadgeObj : PlaceBadge | None = PlaceBadge.query.filter_by( id = badge_id ).first()
    if BadgeObj is None:
        raise ValueError("Badge does not exist.")

    if UserBadge.query.filter_by( badge_id = badge_id, user_id = user_obj.id ).first() is not None:
        raise UserAlreadyHasBadgeException("User already has badge.")

    BadgeAwarded = UserBadge( badge_id = badge_id, user_id = user_obj.id )
    db.session.add( BadgeAwarded )

    try:
        if BadgeObj.asset_reward is not None:
            UserAssetObj : UserAsset | None = UserAsset.query.filter_by( userid = user_obj.id, assetid = BadgeObj.asset_reward ).first()
            if UserAssetObj is None:
                AssetObj : Asset | None = Asset.query.filter_by( id = BadgeObj.asset_reward ).first()
                if AssetObj is None:
                    raise ValueError(f"Asset [{BadgeObj.asset_reward}] does not exist for badge {BadgeObj.id}")

                UserAssetObj = UserAsset( userid = user_obj.id, assetid = BadgeObj.asset_reward )
                db.session.add( UserAssetObj )
    except Exception as e:
        logging.error(f"Error attempting to give user [{user_obj.id}] badge award [{badge_id}]: {e}")

    db.session.commit()

    return BadgeAwarded

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

    return None

csrf.exempt(BadgesAPIRoute)
@BadgesAPIRoute.errorhandler(CSRFError)
def handle_csrf_error(e):
    ErrorResponse = make_response(jsonify({
        "errors": [
            {
                "code": 0,
                "message": "Token Validation Failed"
            }
        ]
    }))

    ErrorResponse.status_code = 403
    ErrorResponse.headers["x-csrf-token"] = generate_csrf()
    return ErrorResponse

@BadgesAPIRoute.errorhandler(429)
def handle_ratelimit_reached(e):
    return jsonify({"errors": [{"code": 9,"message": "The flood limit has been exceeded."}]}), 429
@BadgesAPIRoute.errorhandler(405)
def handle_method_not_allowed(e):
    return jsonify({"errors": [{"code": 0,"message": "MethodNotAllowed"}]}), 405

@BadgesAPIRoute.before_request
def before_request():
    if "Roblox/" not in request.user_agent.string:
        csrf.protect()

@BadgesAPIRoute.route('/v1/badges/<int:badge_id>', methods=['GET'])
@limiter.limit("60/minute")
def get_badge_info( badge_id : int ):
    BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = badge_id ).first()
    if BadgeObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
    
    UniverseObj : Universe = Universe.query.filter_by( id = BadgeObj.universe_id ).first()

    BadgeAwardedCount : int = UserBadge.query.filter_by( badge_id = badge_id ).count()
    PastAwardedCount : int = UserBadge.query.filter_by( badge_id = badge_id ).filter( UserBadge.awarded_at > datetime.utcnow() - timedelta( hours = 24 ) ).count()
    AssociatedPlaceAsset : Asset = Asset.query.filter_by( id = UniverseObj.root_place_id ).first()
    BadgeWinRatePercentage : float = CalculateBadgeRarity( badge_id = badge_id )

    return jsonify({
        "id": BadgeObj.id,
        "name": BadgeObj.name,
        "description": BadgeObj.description,
        "displayName": BadgeObj.name,
        "displayDescription": BadgeObj.description,
        "enabled": BadgeObj.enabled,
        "iconImageId": BadgeObj.icon_image_id,
        "displayIconImageId": BadgeObj.icon_image_id,
        "created": BadgeObj.created_at.isoformat(),
        "updated": BadgeObj.updated_at.isoformat(),
        "statistics": {
            "pastAwardCount": PastAwardedCount,
            "awardCount": BadgeAwardedCount,
            "winRatePercentage": BadgeWinRatePercentage
        },
        "awardingUniverse": {
            "id": UniverseObj.id,
            "name": AssociatedPlaceAsset.name,
            "rootPlaceId": UniverseObj.root_place_id
        }
    })

# This endpoint should only be used by 2020 games, which should always have "AccessKey" in the request headers unlike 2014
@BadgesAPIRoute.route('/v1/users/<int:user_id>/badges/<int:badge_id>/award-badge', methods=["POST"])
@auth.gameserver_accesskey_required
def server_award_badge_route( user_id : int, badge_id : int ):
    BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = badge_id ).first()
    if BadgeObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404

    UserObj : User | None = User.query.filter_by( id = user_id ).first()
    if UserObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is invalid or does not exist."}]}), 404

    PlaceServerPlayerObj : PlaceServerPlayer | None = PlaceServerPlayer.query.filter_by( userid = user_id ).first()
    if PlaceServerPlayerObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400

    PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceServerPlayerObj.serveruuid ).first()
    # This should never happen, but just in case
    if PlaceServerObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
    ServerPlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObj.serverPlaceId ).first()
    if ServerPlaceObj.parent_universe_id != BadgeObj.universe_id:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400

    RequestPlaceId = request.headers.get( key = "Roblox-Place-Id", default = None, type = int )
    if RequestPlaceId is None:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    PlaceObj : Place = Place.query.filter_by( placeid = RequestPlaceId ).first()
    if PlaceObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    if PlaceObj.parent_universe_id != BadgeObj.universe_id:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    
    try:
        AwardBadgeToUser( badge_id = badge_id, user_obj = UserObj )
    except UserAlreadyHasBadgeException:
        return jsonify({"errors": [{"code": 1,"message": "User already has badge."}]}), 400
    except ValueError:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
    except Exception as e:
        logging.error(f"Error awarding badge to user: {e}")
        return jsonify({"errors": [{"code": 1,"message": "Internal Server error."}]}), 500
    
    return jsonify({ "success": True })

# 2018 RCC
@BadgesAPIRoute.route('/assets/award-badge', methods=["POST"])
@auth.gameserver_accesskey_required
def server_award_badge_route_legacy():
    reqUserId = request.args.get("userId", type=int, default=None)
    reqBadgeId = request.args.get("badgeId", type=int, default=None)
    reqPlaceId = request.args.get("placeId", type=int, default=None)

    if reqUserId is None or reqBadgeId is None or reqPlaceId is None:
        return jsonify({"errors": [{"code": 1,"message": "Invalid request."}]}), 400

    BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = reqBadgeId ).first()
    if BadgeObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404

    UserObj : User | None = User.query.filter_by( id = reqUserId ).first()
    if UserObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is invalid or does not exist."}]}), 404

    PlaceServerPlayerObj : PlaceServerPlayer | None = PlaceServerPlayer.query.filter_by( userid = reqUserId ).first()
    if PlaceServerPlayerObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400

    PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceServerPlayerObj.serveruuid ).first()
    # This should never happen, but just in case
    if PlaceServerObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
    ServerPlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObj.serverPlaceId ).first()
    if ServerPlaceObj.parent_universe_id != BadgeObj.universe_id:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400

    RequestPlaceId = request.headers.get( key = "Roblox-Place-Id", default = None, type = int )
    if RequestPlaceId is None:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    if RequestPlaceId != reqPlaceId:
        return jsonify({"errors": [{"code": 1,"message": f"Roblox-Place-Id [{RequestPlaceId}] Header does not match placeId argument [{reqPlaceId}]"}]}), 400
    PlaceObj : Place = Place.query.filter_by( placeid = RequestPlaceId ).first()
    if PlaceObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    if PlaceObj.parent_universe_id != BadgeObj.universe_id:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404

    try:
        AwardBadgeToUser( badge_id = reqBadgeId, user_obj = UserObj )
    except UserAlreadyHasBadgeException:
        return jsonify({"errors": [{"code": 1,"message": "User already has badge."}]}), 400
    except ValueError:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
    except Exception as e:
        logging.error(f"Error awarding badge to user: {e}")
        return jsonify({"errors": [{"code": 1,"message": "Internal Server error."}]}), 500
    PlaceAssetObj : Asset = Asset.query.filter_by( id = PlaceObj.placeid ).first()
    PlaceCreator : User | Group = GetAssetCreator( PlaceAssetObj )

    return f"{UserObj.username} won {PlaceCreator.username if PlaceAssetObj.creator_type == 0 else PlaceCreator.name}'s \"{BadgeObj.name}\" award!" # This message is sent to client to be shown as a badge awarded notification

@BadgesAPIRoute.route("/Game/Badge/HasBadge.ashx", methods=["GET"])
@auth.gameserver_authenticated_required # Only IP address is checked
def query_user_has_badge():
    reqUserId = request.args.get("UserID", type=int, default=None)
    reqBadgeId = request.args.get("BadgeID", type=int, default=None)

    if reqUserId is None or reqBadgeId is None:
        return "0"

    BadgeObj : PlaceBadge | None = PlaceBadge.query.filter_by( id = reqBadgeId ).first()
    if BadgeObj is None:
        return "0"

    UserObj : User | None = User.query.filter_by( id = reqUserId ).first()
    if UserObj is None:
        return "0"

    if UserBadge.query.filter_by( badge_id = reqBadgeId, user_id = reqUserId ).first() is None:
        return "0"

    return "1"

@BadgesAPIRoute.route("/Game/Badge/AwardBadge.ashx", methods=["POST"])
@auth.gameserver_authenticated_required
def award_badge_to_user():
    reqUserId = request.args.get("UserID", type=int, default=None)
    reqBadgeId = request.args.get("BadgeID", type=int, default=None)
    reqPlaceId = request.args.get("PlaceID", type=int, default=None)

    if reqUserId is None or reqBadgeId is None or reqPlaceId is None:
        return jsonify({"errors": [{"code": 1,"message": "Invalid request."}]}), 400
    
    BadgeObj : PlaceBadge = PlaceBadge.query.filter_by( id = reqBadgeId ).first()
    if BadgeObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
    
    UserObj : User | None = User.query.filter_by( id = reqUserId ).first()
    if UserObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is invalid or does not exist."}]}), 404
    
    PlaceServerPlayerObj : PlaceServerPlayer | None = PlaceServerPlayer.query.filter_by( userid = reqUserId ).first()
    if PlaceServerPlayerObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
    
    PlaceServerObj : PlaceServer = PlaceServer.query.filter_by( serveruuid = PlaceServerPlayerObj.serveruuid ).first()
    # This should never happen, but just in case
    if PlaceServerObj is None:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
    ServerPlaceObj : Place = Place.query.filter_by( placeid = PlaceServerObj.serverPlaceId ).first()
    if ServerPlaceObj.parent_universe_id != BadgeObj.universe_id:
        return jsonify({"errors": [{"code": 1,"message": "User is not in the game."}]}), 400
    
    PlaceObj : Place = Place.query.filter_by( placeid = reqPlaceId ).first()
    if PlaceObj is None:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    if PlaceObj.parent_universe_id != BadgeObj.universe_id:
        return jsonify({"errors": [{"code": 1,"message": "Place ID is invalid or does not exist."}]}), 404
    
    try:
        AwardBadgeToUser( badge_id = reqBadgeId, user_obj = UserObj )
    except UserAlreadyHasBadgeException:
        return jsonify({"errors": [{"code": 1,"message": "User already has badge."}]}), 400
    except ValueError:
        return jsonify({"errors": [{"code": 1,"message": "Badge is invalid or does not exist."}]}), 404
    except Exception as e:
        logging.error(f"Error awarding badge to user: {e}")
        return jsonify({"errors": [{"code": 1,"message": "Internal Server error."}]}), 500
    PlaceAssetObj : Asset = Asset.query.filter_by( id = PlaceObj.placeid ).first()
    PlaceCreator : User | Group = GetAssetCreator( PlaceAssetObj )

    return f"{UserObj.username} won {PlaceCreator.username if PlaceAssetObj.creator_type == 0 else PlaceCreator.name}'s \"{BadgeObj.name}\" award!" # This message is sent to client to be shown as a badge awarded notification

