from flask import Blueprint, request, jsonify, abort
from models import db, LEADERBOARD_MAP, LEADERBOARD_CAP
from auth import require_api_key, require_game_key

bp = Blueprint("leaderboard", __name__)


def get_model(leaderboard):
    """Return the model class for the given leaderboard name, or 404."""
    model = LEADERBOARD_MAP.get(leaderboard)
    if not model:
        abort(404, description=f"Leaderboard '{leaderboard}' not found.")
    return model


@bp.route("/add", methods=["POST"])
@require_game_key
def add_score():
    data = request.get_json(silent=True)
    if not data:
        return jsonify({"error": "Request body must be JSON."}), 400

    name = data.get("name")
    score = data.get("score")

    # Reject booleans explicitly — bool is a subclass of int in Python
    if not name or not isinstance(name, str) or not name.strip():
        return jsonify({"error": "'name' must be a non-empty string."}), 400
    if score is None or isinstance(score, bool) or not isinstance(score, int):
        return jsonify({"error": "'score' must be an integer."}), 400

    name = name.strip()

    # Insert into every leaderboard table simultaneously
    for model in LEADERBOARD_MAP.values():
        db.session.add(model(name=name, score=score))
    db.session.commit()

    # Rank = number of entries with a strictly higher score, plus one (per table)
    ranks = {
        board: db.session.query(model).filter(model.score > score).count() + 1
        for board, model in LEADERBOARD_MAP.items()
    }

    return jsonify(ranks), 201


@bp.route("/<leaderboard>/prune", methods=["POST"])
@require_api_key
def prune(leaderboard):
    model = get_model(leaderboard)

    # Collect the IDs of entries we want to keep: top LEADERBOARD_CAP by score,
    # with ties broken by newest created timestamp so older low scores are evicted first
    keep_ids = [
        row.id
        for row in db.session.query(model.id)
        .order_by(model.score.desc(), model.created.desc())
        .limit(LEADERBOARD_CAP)
        .all()
    ]

    # Nothing to prune if we're still under the cap
    if len(keep_ids) < LEADERBOARD_CAP:
        return jsonify({"pruned": 0}), 200

    pruned = (
        db.session.query(model)
        .filter(model.id.not_in(keep_ids))
        .delete(synchronize_session="fetch")
    )
    db.session.commit()

    return jsonify({"pruned": pruned}), 200


@bp.route("/<leaderboard>/top/<int:n>", methods=["GET"])
def top_scores(leaderboard, n):
    model = get_model(leaderboard)

    # Clamp n so callers can't request arbitrarily large result sets
    n = max(1, min(n, LEADERBOARD_CAP))

    entries = (
        db.session.query(model)
        .order_by(model.score.desc(), model.created.asc())
        .limit(n)
        .all()
    )

    return jsonify({"leaderboard": leaderboard, "entries": [e.to_dict() for e in entries]}), 200


@bp.route("/<leaderboard>/player/<name>", methods=["GET"])
def player_best(leaderboard, name):
    model = get_model(leaderboard)

    entry = (
        db.session.query(model)
        .filter(model.name == name)
        .order_by(model.score.desc())
        .first()
    )

    if not entry:
        return jsonify({"error": f"No entry found for player '{name}'."}), 404

    return jsonify(entry.to_dict()), 200


@bp.route("/<leaderboard>/all", methods=["DELETE"])
@require_api_key
def clear_leaderboard(leaderboard):
    model = get_model(leaderboard)
    db.session.query(model).delete()
    db.session.commit()
    return jsonify({"deleted": True}), 200


@bp.route("/<leaderboard>/<int:entry_id>", methods=["DELETE"])
@require_api_key
def delete_entry(leaderboard, entry_id):
    model = get_model(leaderboard)

    entry = db.session.get(model, entry_id)
    if not entry:
        return jsonify({"error": f"Entry {entry_id} not found."}), 404

    db.session.delete(entry)
    db.session.commit()
    return jsonify({"deleted": True}), 200
