"""Web blueprint: main app pages."""
import os
import uuid
from flask import Blueprint, request, redirect, url_for, render_template, session, abort, flash, current_app
from datetime import datetime, timedelta, date
from app.extensions import db, login_manager
from app.models.user import User, GameProfile
from app.models.game import Game, GameMode
from app.models.team import Team, TeamMember
from app.models.match import Match
from app.models.wallet import Wallet, Transaction
from app.services.leaderboard_service import compute_leaderboard
from app.models.leaderboard import PlayerStat

bp = Blueprint("web", __name__)


@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))


def require_login(f):
    from functools import wraps
    @wraps(f)
    def decorated(*args, **kwargs):
        if "user_id" not in session:
            return redirect(url_for("auth.login"))
        return f(*args, **kwargs)
    return decorated


@bp.before_request
def check_language():
    if "user_id" in session and request.endpoint and not request.endpoint.startswith("auth.") and not request.endpoint.startswith("public.") and not request.endpoint.startswith("admin."):
        user = db.session.get(User, session["user_id"])
        if user and user.lang:
            session["lang"] = user.lang


@bp.route("/home")
@require_login
def home():
    """Main Web App page - shows games + My Rooms."""
    user = db.session.get(User, session["user_id"])
    games = Game.query.filter_by(is_active=True).order_by(Game.sort_order).all()

    from app.models.match import Match, MatchParticipant
    from datetime import datetime

    today = datetime.utcnow().date()
    tomorrow = datetime.combine(today, datetime.max.time())

    my_rooms = (MatchParticipant.query
        .filter_by(user_id=user.id)
        .filter(MatchParticipant.status.in_(["registered", "confirmed"]))
        .join(Match)
        .filter(Match.starts_at >= datetime.utcnow())
        .order_by(Match.starts_at)
        .limit(10)
        .all())

    active_creds = []
    for p in my_rooms:
        m = p.match
        if m.credentials_published and m.room_code:
            active_creds.append(m)

    registered_ids = set(
        p.match_id for p in
        MatchParticipant.query.filter_by(user_id=user.id)
        .filter(MatchParticipant.status.in_(["registered", "confirmed"])).all()
    )

    # Tournament data for premium home page
    now = datetime.utcnow()
    upcoming_matches = Match.query.filter(
        Match.status.in_(["upcoming", "live"]),
        Match.starts_at >= now
    ).order_by(Match.starts_at).limit(12).all()

    live_tournaments = Match.query.filter(Match.status == "live").count()

    # Count online players across live matches
    online_players = 0
    for m in Match.query.filter(Match.status == "live").all():
        online_players += m.current_players

    featured_match = Match.query.filter_by(is_special=True, status="upcoming").order_by(Match.starts_at).first()

    # Leaderboard preview (global + weekly + monthly windows)
    leaderboard_entries, weekly_entries, monthly_entries = [], [], []
    try:
        gid = games[0].id if games else 1
        leaderboard_entries = compute_leaderboard(gid, days=None)[:10]
        weekly_entries = compute_leaderboard(gid, days=7)[:10]
        monthly_entries = compute_leaderboard(gid, days=30)[:10]
    except Exception:
        pass

    return render_template("webapp/home.html", user=user, games=games,
                           my_rooms=my_rooms, active_creds=active_creds,
                           registered_match_ids=registered_ids,
                           upcoming_matches=upcoming_matches,
                           live_tournaments=live_tournaments,
                           online_players=online_players,
                           featured_match=featured_match,
                           leaderboard_entries=leaderboard_entries,
                           weekly_entries=weekly_entries,
                           monthly_entries=monthly_entries)


@bp.route("/intro")
@require_login
def intro():
    """Tutorial video page - shows video for selected language."""
    user = db.session.get(User, session["user_id"])
    lang = session.get("lang", "en")
    # Map language to video URL
    video_map = {
        "en": "/static/videos/tutorial_en.mp4",
        "fa": "/static/videos/tutorial_fa.mp4",
        "tr": "/static/videos/tutorial_tr.mp4",
        "ar": "/static/videos/tutorial_ar.mp4",
    }
    # Only pass a video URL if the file actually exists (otherwise the
    # page used to render a dead <video> that 404'd and looked broken)
    video_url = None
    candidate = video_map.get(lang, video_map["en"])
    if candidate:
        rel_path = candidate.replace("/static/", "", 1)
        if os.path.isfile(os.path.join(current_app.static_folder, rel_path)):
            video_url = candidate
    return render_template("webapp/intro.html", user=user, video_url=video_url)


@bp.route("/intro/done", methods=["POST"])
@require_login
def intro_done():
    user = db.session.get(User, session["user_id"])
    user.intro_seen = True
    db.session.commit()
    return redirect(url_for("web.home"))


@bp.route("/game/<slug>")
@require_login
def game_detail(slug):
    """Game detail page - shows modes and rooms for the selected game."""
    user = db.session.get(User, session["user_id"])
    game = Game.query.filter_by(slug=slug, is_active=True).first_or_404()
    modes = GameMode.query.filter_by(game_id=game.id, is_active=True).order_by(GameMode.id).all()
    from app.models.match import Match, MatchParticipant
    from app.models.team import Team, TeamMember
    registered_ids = set(
        r.match_id for r in MatchParticipant.query.filter_by(user_id=user.id).filter(MatchParticipant.status.in_(["registered","confirmed"])).all()
    )
    # Region: players ONLY see matches of their own region (strict)
    user_region = user.region or "global"
    region_filter = (request.args.get("region") or "").strip()
    modes_with_matches = []
    all_matches = []
    for mode in modes:
        q = Match.query.filter(
            Match.game_id == game.id,
            Match.mode_id == mode.id,
            Match.status.in_(["upcoming", "live"])
        )
        if region_filter and region_filter != "all":
            q = q.filter(Match.region == region_filter)
        elif not region_filter:
            # strict: only matches in the player's region
            q = q.filter(Match.region == user_region)
        matches = q.order_by(Match.starts_at).limit(20).all()
        modes_with_matches.append({"mode": mode, "matches": matches})
        all_matches.extend(matches)
    user_teams = Team.query.join(TeamMember).filter(
        TeamMember.user_id == user.id,
        Team.game_id == game.id
    ).all()
    return render_template("webapp/game.html", user=user, game=game,
                           modes_with_matches=modes_with_matches,
                           registered_match_ids=registered_ids,
                           user_teams=user_teams,
                           matches=all_matches)


@bp.route("/team")
@require_login
def team():
    user = db.session.get(User, session["user_id"])
    game_id = request.args.get("game_id", type=int)
    games = Game.query.filter_by(is_active=True).all()
    if not game_id:
        game_id = games[0].id if games else 1
    game = db.session.get(Game, game_id)
    # Per-game team (global team concept removed - each game has own team)
    # Lookup order: 1) a team the user JOINED (accepted invite, not owner),
    # 2) a team the user OWNS, 3) auto-create a fresh one.
    # Joined must win over owned: after accepting an invite the user must
    # land inside the joined roster, not on their old auto-created team.
    team = (Team.query
            .join(TeamMember, TeamMember.team_id == Team.id)
            .filter(TeamMember.user_id == user.id,
                    TeamMember.status == "active",
                    Team.owner_id != user.id,
                    Team.game_id == game_id)
            .first())
    if not team:
        team = Team.query.filter_by(game_id=game_id, owner_id=user.id).first()
    if not team:
        team = Team(game_id=game_id, name=f"{user.display_name}'s Team", owner_id=user.id)
        db.session.add(team)
        db.session.flush()
        db.session.add(TeamMember(team_id=team.id, user_id=user.id, role="owner"))
        db.session.commit()
        team = Team.query.filter_by(id=team.id).first()
    modes = GameMode.query.filter_by(game_id=game_id, is_active=True).all()
    # Invites for this game's team only
    from app.models.team import TeamInvite
    invites_received = TeamInvite.query.join(Team).filter(
        TeamInvite.to_user_id == user.id,
        TeamInvite.status == "pending",
        Team.game_id == game_id
    ).order_by(TeamInvite.created_at.desc()).all()
    invites_sent = TeamInvite.query.join(Team).filter(
        TeamInvite.from_user_id == user.id,
        TeamInvite.status == "pending",
        Team.game_id == game_id
    ).order_by(TeamInvite.created_at.desc()).all()
    return render_template("webapp/team.html", user=user, game=game, team=team,
                           members=team.members, modes=modes, games=games,
                           current_game_id=game_id,
                           invites_received=invites_received, invites_sent=invites_sent)


@bp.route("/wallet")
@require_login
def wallet():
    user = db.session.get(User, session["user_id"])
    wallets = Wallet.query.filter_by(user_id=user.id).all()
    transactions = []
    for w in wallets:
        transactions.extend(w.transactions.limit(20).all())
    transactions.sort(key=lambda x: x.created_at, reverse=True)
    transactions = transactions[:50]
    return render_template("webapp/wallet.html", user=user, wallets=wallets, transactions=transactions)


@bp.route("/leaderboard")
@require_login
def leaderboard():
    user = db.session.get(User, session["user_id"])
    games = Game.query.filter_by(is_active=True).all()
    game_id = request.args.get("game_id", type=int)
    if not game_id and games:
        game_id = games[0].id
    game = db.session.get(Game, game_id) if game_id else None
    entries, weekly_entries, monthly_entries = [], [], []
    if game:
        # Three time windows rendered server-side → tab switching is instant
        entries = compute_leaderboard(game.id, days=None)            # GLOBAL (all-time)
        weekly_entries = compute_leaderboard(game.id, days=7)        # last 7 days
        monthly_entries = compute_leaderboard(game.id, days=30)      # last 30 days
    return render_template("webapp/leaderboard.html", user=user, games=games, game=game,
                           entries=entries, weekly_entries=weekly_entries,
                           monthly_entries=monthly_entries)


@bp.route("/search")
@require_login
def search():
    user = db.session.get(User, session["user_id"])
    games = Game.query.filter_by(is_active=True).all()
    mode_id = request.args.get("mode_id", type=int)
    game_id = request.args.get("game_id", type=int)
    start_time = request.args.get("start_time", "")
    end_time = request.args.get("end_time", "")
    modes = GameMode.query.filter_by(game_id=game_id, is_active=True).all() if game_id else []
    q = Match.query.filter_by(status="upcoming")
    if game_id:
        q = q.filter_by(game_id=game_id)
    if mode_id:
        q = q.filter_by(mode_id=mode_id)
    if start_time:
        try:
            st = datetime.fromisoformat(start_time)
            q = q.filter(Match.starts_at >= st)
        except Exception:
            pass
    if end_time:
        try:
            et = datetime.fromisoformat(end_time)
            q = q.filter(Match.starts_at <= et)
        except Exception:
            pass
    # Region strict filter: user only sees matches of their own region
    user_region = user.region or "global"
    q = q.filter(Match.region == user_region)
    matches = q.order_by(Match.starts_at).all()
    return render_template("webapp/search.html", user=user, games=games, matches=matches, mode_id=mode_id, game_id=game_id, start_time=start_time, end_time=end_time, user_region=user_region)


@bp.route("/special")
@require_login
def special():
    user = db.session.get(User, session["user_id"])
    games = Game.query.filter_by(is_active=True).all()
    matches = Match.query.filter_by(is_special=True, status="upcoming").order_by(Match.starts_at).all()
    return render_template("webapp/special.html", user=user, games=games, matches=matches)


@bp.route("/profile")
@require_login
def profile():
    user = db.session.get(User, session["user_id"])
    games = Game.query.filter_by(is_active=True).all()
    current_theme = session.get("theme", "dark")
    all_stats = PlayerStat.query.filter_by(user_id=user.id).all()
    total_matches = sum(s.matches_played for s in all_stats)
    total_wins = sum(s.wins for s in all_stats)
    total_played = total_matches if total_matches else 1
    win_rate = round((total_wins / total_played) * 100, 2) if total_played else 0
    stats = {
        "wins": total_wins,
        "matches": total_matches,
        "win_rate": win_rate,
        "prize_earned": 0,
    }
    game_profiles = {p.game_id: p for p in user.game_profiles}
    return render_template("webapp/profile.html", user=user, games=games,
                           game_profiles=game_profiles, stats=stats,
                           current_lang=session.get("lang", "en"),
                           current_theme=current_theme)


@bp.route("/settings")
@require_login
def settings_page():
    return redirect(url_for("web.profile"))


@bp.route("/theme/save", methods=["POST"])
@require_login
def theme_save():
    user = db.session.get(User, session["user_id"])
    theme = request.form.get("theme", "dark")
    if theme in ("dark", "light"):
        user.theme = theme
        session["theme"] = theme
        db.session.commit()
    return redirect(url_for("web.profile"))


@bp.route("/region/set")
@require_login
def region_set():
    user = db.session.get(User, session["user_id"])
    region = request.args.get("region", "global")
    valid = ("global", "ir", "tr", "eu", "na", "sa", "sea", "ea", "mena", "ru")
    if user and region in valid:
        user.region = region
        db.session.commit()
    session["region"] = region
    nxt = request.args.get("next") or url_for("web.home")
    if not nxt.startswith("/"):
        nxt = url_for("web.home")
    return redirect(nxt)


@bp.route("/profile/update", methods=["POST"])
@require_login
def profile_update():
    user = db.session.get(User, session["user_id"])
    user.username = request.form.get("username", "").strip()[:60] or user.username
    user.bio = request.form.get("bio", "").strip()[:300]
    user.steam_name = request.form.get("steam_name", "").strip()[:60]
    if "avatar" in request.files:
        f = request.files["avatar"]
        if f and f.filename:
            import os, uuid
            from werkzeug.utils import secure_filename
            ext = f.filename.rsplit(".", 1)[1].lower()
            if ext in ("png", "jpg", "jpeg", "gif", "webp"):
                name = f"av_{uuid.uuid4().hex[:12]}.{ext}"
                path = os.path.join(current_app.config["UPLOAD_FOLDER"], secure_filename(name))
                f.save(path)
                user.avatar = name
    db.session.commit()
    flash("Profile updated", "success")
    return redirect(url_for("web.profile"))


@bp.route("/profile/password", methods=["POST"])
@require_login
def profile_password():
    user = db.session.get(User, session["user_id"])
    current = request.form.get("current_password", "")
    new = request.form.get("new_password", "")
    if not user.check_password(current):
        flash("Current password is incorrect", "danger")
    elif len(new) < 6:
        flash("New password must be at least 6 characters", "danger")
    else:
        user.set_password(new)
        db.session.commit()
        flash("Password changed successfully", "success")
    return redirect(url_for("web.profile"))


@bp.route("/game-profile/save", methods=["POST"])
@require_login
def game_profile_save():
    user = db.session.get(User, session["user_id"])
    game_id = request.form.get("game_id", type=int)
    if not game_id:
        flash("Invalid game", "danger")
        return redirect(url_for("web.profile"))
    gp = GameProfile.query.filter_by(user_id=user.id, game_id=game_id).first()
    if not gp:
        gp = GameProfile(user_id=user.id, game_id=game_id)
        db.session.add(gp)
    gp.in_game_name = request.form.get("in_game_name", "").strip()[:60]
    gp.in_game_id = request.form.get("in_game_id", "").strip()[:60]
    db.session.commit()
    flash("Game profile saved", "success")
    return redirect(url_for("web.profile"))


@bp.route("/match/<int:match_id>/join", methods=["GET", "POST"])
@require_login
def match_join(match_id):
    from app.models.match import Match, MatchParticipant, MatchSlotParticipant
    from app.models.team import Team, TeamMember
    from app.models.wallet import Wallet
    from app.models.notification import Notification
    import json

    m = db.session.get(Match, match_id)
    if not m:
        abort(404)

    user = db.session.get(User, session["user_id"])
    existing = MatchParticipant.query.filter_by(match_id=m.id, user_id=user.id).first()

    # ── Registration window rule (Task 14 — flipped) ──
    # A match whose start time has passed is never registrable. By default
    # registration OPENS at 12 midnight (00:00) of the match day (the midnight
    # of the night before the match day); before that moment it is not open
    # yet. Admin-selected exception (reg_open_until_start): no opening gate.
    reg_closed = m.registration_closed and not existing
    reg_opens_at = m.registration_opens_at
    reg_until_start = bool(m.reg_open_until_start)
    reg_closed_reason = ""
    if m.registration_closed:
        if datetime.utcnow() >= m.starts_at:
            reg_closed_reason = "started"
        elif m.status in ("finished", "cancelled", "live"):
            reg_closed_reason = "status"
        else:
            reg_closed_reason = "not_open_yet"

    def _entry_tx(uid, wallet, fee):
        """Visible per-player entry-fee ledger row (each member pays separately)."""
        if fee <= 0 or not wallet:
            return None
        return Transaction(
            ref=f"ENTRY-{m.id}-{uid}-{uuid.uuid4().hex[:8].upper()}",
            user_id=uid, wallet_id=wallet.id,
            amount=-int(fee), kind="entry",
            reason=f"Entry - {m.title}", status="completed",
        )

    entry_wallet = Wallet.query.filter_by(user_id=user.id, kind="deposit").first()
    entry_balance = entry_wallet.balance if entry_wallet else 0
    insufficient_balance = m.entry_fee > 0 and entry_balance < m.entry_fee
    currency = "USDT"

    # Slot config
    slot_config = m.slot_config if hasattr(m, 'slot_config') else {}
    slot_groups = slot_config.get('groups', [])
    if not slot_groups and slot_config.get('total_slots'):
        # Legacy single-group layout -> normalize to one unnamed group
        try:
            slot_groups = [{"name": "", "slots": int(slot_config["total_slots"]),
                            "players_per_slot": int(slot_config.get("players_per_slot", 1))}]
        except Exception:
            slot_groups = []
    occupied_slots = [sp for sp in MatchSlotParticipant.query.filter_by(match_id=m.id).all()]
    participant_slot_index = existing.selected_slot_index if existing else None
    participant_slot_group = existing.selected_slot_group if existing else None

    # Who sits in which slot (for avatar + username under each slot)
    slot_occupants = {}
    for sp in occupied_slots:
        if sp.user:
            slot_occupants[f"{sp.group_name or ''}|{sp.slot_index}"] = {
                "name": (sp.user.display_name or sp.user.username or "Player"),
                "avatar": sp.user.avatar or "",
                "numeric_id": sp.user.numeric_id,
            }

    # My team for this game (joined-wins, same rule as the team page) + member cards
    our_team = (Team.query
                .join(TeamMember, TeamMember.team_id == Team.id)
                .filter(TeamMember.user_id == user.id,
                        TeamMember.status == "active",
                        Team.owner_id != user.id,
                        Team.game_id == m.game_id)
                .first())
    if not our_team:
        our_team = Team.query.filter_by(owner_id=user.id, game_id=m.game_id).first()
    my_team_members = []
    if our_team:
        from app.models.wallet import Wallet as _W
        for tm in our_team.members:
            if tm.status != "active" or not tm.user:
                continue
            w = _W.query.filter_by(user_id=tm.user_id, kind="deposit").first()
            bal = w.balance if w else 0
            my_team_members.append({
                "id": tm.user_id,
                "name": (tm.user.display_name or tm.user.username or "Player"),
                "avatar": tm.user.avatar or "",
                "numeric_id": tm.user.numeric_id,
                "balance": bal,
                "ok": bool(bal >= m.entry_fee),
                "is_me": tm.user_id == user.id,
                "role": tm.role,
                "registered": bool(MatchParticipant.query.filter_by(match_id=m.id, user_id=tm.user_id).first()),
            })

    user_teams = []
    insufficient_team_ids = []
    team_readiness = []
    if m.team_mode:
        teams = Team.query.join(TeamMember).filter(
            TeamMember.user_id == user.id,
            Team.game_id == m.game_id
        ).all()
        for t in teams:
            members_status = []
            ready = True
            if t.member_count < m.mode.squad_size:
                ready = False
                insufficient_team_ids.append(t.id)
            for tm in t.members:
                if tm.status != "active":
                    continue
                w = Wallet.query.filter_by(user_id=tm.user_id, kind="deposit").first()
                ok = bool(w and w.balance >= m.entry_fee)
                members_status.append({
                    "id": tm.user_id,
                    "name": tm.user.display_name if tm.user else "Player",
                    "avatar": (tm.user.avatar or "") if tm.user else "",
                    "numeric_id": tm.user.numeric_id if tm.user else "",
                    "balance": w.balance if w else 0,
                    "ok": ok,
                    "role": tm.role,
                    "registered": bool(MatchParticipant.query.filter_by(match_id=m.id, user_id=tm.user_id).first()),
                })
                if not ok:
                    insufficient_team_ids.append(t.id)
            team_readiness.append({
                "team": t,
                "members": members_status,
                "size_ok": t.member_count >= m.mode.squad_size,
                "wallet_ok": all(x["ok"] for x in members_status),
            })
        user_teams = teams

    conflict_match = None
    if not existing:
        conflict = MatchParticipant.query.join(Match).filter(
            MatchParticipant.user_id == user.id,
            MatchParticipant.status.in_(["registered", "confirmed"]),
            Match.id != m.id,
            Match.starts_at == m.starts_at
        ).first()
        if conflict:
            conflict_match = conflict.match

    registered_at = existing.registered_at if existing else None

    # For non-team matches: also check if user has a team they want to use
    if not m.team_mode:
        teams = Team.query.join(TeamMember).filter(
            TeamMember.user_id == user.id,
            Team.game_id == m.game_id
        ).all()
        for t in teams:
            members_status = []
            size_ok = t.member_count >= m.mode.squad_size if m.mode.squad_size > 1 else True
            wallet_ok = True
            insufficient_team_ids.append(t.id)  # default to insufficient
            for tm in t.members:
                if tm.status != "active":
                    continue
                w = Wallet.query.filter_by(user_id=tm.user_id, kind="deposit").first()
                ok = bool(w and w.balance >= m.entry_fee)
                members_status.append({
                    "id": tm.user_id,
                    "name": tm.user.display_name if tm.user else "Player",
                    "avatar": (tm.user.avatar or "") if tm.user else "",
                    "numeric_id": tm.user.numeric_id if tm.user else "",
                    "balance": w.balance if w else 0,
                    "ok": ok,
                    "role": tm.role,
                    "registered": bool(MatchParticipant.query.filter_by(match_id=m.id, user_id=tm.user_id).first()),
                })
                if not ok:
                    wallet_ok = False
            if size_ok and wallet_ok:
                insufficient_team_ids.remove(t.id)
            team_readiness.append({
                "team": t,
                "members": members_status,
                "size_ok": size_ok,
                "wallet_ok": wallet_ok,
            })
        if teams:
            user_teams = teams

    if request.method == "POST":
        if m.registration_closed:
            if datetime.utcnow() >= m.starts_at:
                flash("This match has already started — registration is closed.", "danger")
            else:
                flash("Registration for this match is not open yet.", "danger")
            return redirect(url_for("web.match_join", match_id=m.id))

        # Task 14: registration must pass through the rules gate — the player
        # has to see and accept the admin-written rules before joining.
        if request.form.get("rules_accepted") != "1":
            flash("You must read and accept the match rules before registering.", "danger")
            return redirect(url_for("web.match_join", match_id=m.id))

        if existing:
            flash("Already registered", "warning")
            return redirect(url_for("web.game_detail", slug=m.game.slug))

        if conflict_match:
            flash(f"Schedule conflict: you are registered in {conflict_match.title} at the same time.", "danger")
            return redirect(url_for("web.match_join", match_id=m.id))

        if conflict_match:
            flash(f"Schedule conflict: you are registered in {conflict_match.title} at the same time.", "danger")
            return redirect(url_for("web.match_join", match_id=m.id))

        # Region guard: block joining matches outside the player's region
        user_region = user.region or "global"
        if m.region and m.region != "global" and user_region != "global" and m.region != user_region:
            flash("This match is in a different region than yours.", "danger")
            return redirect(url_for("web.game_detail", slug=m.game.slug))

        join_type = request.form.get("join_type", "solo")

        if join_type == "solo":
            if insufficient_balance:
                flash(f"Insufficient Entry Wallet balance. You have {entry_balance}, need {m.entry_fee}.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))

            # Handle slot selection (multi-group)
            slot_group_name = request.form.get("slot_group", "").strip()
            slot_index_raw = request.form.get("slot_index", "").strip()
            selected_slot_idx = int(slot_index_raw) if slot_index_raw.isdigit() else None
            
            if slot_groups:
                # Find the group
                selected_group = None
                for g in slot_groups:
                    if g.get('name', '').lower() == slot_group_name.lower():
                        selected_group = g
                        break
                if not selected_group:
                    flash("Invalid slot group selected.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
                total_in_group = selected_group.get('slots', 0)
                if selected_slot_idx is None or selected_slot_idx < 0 or selected_slot_idx >= total_in_group:
                    flash("Please select a valid slot position.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
                # Check slot not already taken in this group
                if MatchSlotParticipant.query.filter_by(match_id=m.id, group_name=slot_group_name, slot_index=selected_slot_idx).first():
                    flash("This slot is already taken!", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
            elif slot_config and slot_config.get('total_slots'):
                # Legacy single-group format
                total = int(slot_config["total_slots"])
                if selected_slot_idx is None or selected_slot_idx < 0 or selected_slot_idx >= total:
                    flash("Please select a valid slot position.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
                if MatchSlotParticipant.query.filter_by(match_id=m.id, slot_index=selected_slot_idx).first():
                    flash("This slot is already taken!", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))

            if m.entry_fee > 0 and entry_wallet:
                entry_wallet.balance -= m.entry_fee
                entry_wallet.last_updated = datetime.utcnow()
                db.session.add(_entry_tx(user.id, entry_wallet, m.entry_fee))

            participant = MatchParticipant(
                match_id=m.id, user_id=user.id,
                status="registered", paid=bool(m.entry_fee == 0 or not insufficient_balance),
                selected_slot_group=slot_group_name if slot_groups else None,
                selected_slot_index=selected_slot_idx,
            )
            db.session.add(participant)
            # Reserve slot if configured
            if slot_groups and slot_group_name and selected_slot_idx is not None:
                sp = MatchSlotParticipant(match_id=m.id, user_id=user.id, 
                                          group_name=slot_group_name, slot_index=selected_slot_idx)
                db.session.add(sp)
            notif = Notification(
                user_id=user.id, type="match_registration",
                title="Registered for Match",
                body=f"You joined {m.title} ({m.game.name})." + (f" Slot #{selected_slot_idx+1} ({slot_group_name})." if slot_group_name and selected_slot_idx is not None else "")
            )
            db.session.add(notif)
            db.session.commit()
            flash("Joined successfully!", "success")
            return redirect(url_for("web.match_join", match_id=m.id))

        elif join_type == "team":
            team_id = request.form.get("team_id", type=int)
            if not team_id:
                flash("Please select a team.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            team = db.session.get(Team, team_id)
            if not team or team.game_id != m.game_id:
                flash("Invalid team.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            # The registering user must belong to this team
            if not TeamMember.query.filter_by(team_id=team.id, user_id=user.id, status="active").first() \
               and team.owner_id != user.id:
                flash("You are not a member of this team.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))

            # ── Selective member registration ──
            member_ids = request.form.getlist("member_ids", type=int) or []
            if not member_ids:
                flash("Select at least one team member to register.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            if len(set(member_ids)) != len(member_ids):
                flash("Duplicate member selection.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            active_member_ids = [tm.user_id for tm in team.members if tm.status == "active"]
            for uid in member_ids:
                if uid not in active_member_ids:
                    flash("One of the selected players is not in your team.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
            # Someone already registered?
            for uid in member_ids:
                if MatchParticipant.query.filter_by(match_id=m.id, user_id=uid).first():
                    u = db.session.get(User, uid)
                    flash(f"{(u.display_name if u else 'Player')} is already registered in this match.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
            # Team-mode matches still need a full squad
            if m.team_mode and len(member_ids) < m.mode.squad_size:
                flash(f"Team mode requires at least {m.mode.squad_size} players.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            remaining = m.capacity - m.current_players
            if len(member_ids) > remaining:
                flash(f"Not enough slots. Only {remaining} remaining.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))

            # ── Per-player slot assignment (every slot holds exactly 1 player) ──
            slot_map = {}
            slot_map_raw = request.form.get("slot_map", "")
            if slot_map_raw:
                try:
                    slot_map = json.loads(slot_map_raw)
                except Exception:
                    flash("Invalid slot assignment data.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
            assignments = {}  # uid -> (group_name, slot_index)
            if slot_groups:
                for key, uid in slot_map.items():
                    gname, _, idx_raw = str(key).rpartition("|")
                    try:
                        idx = int(idx_raw)
                        uid = int(uid)
                    except (ValueError, TypeError):
                        flash("Invalid slot assignment.", "danger")
                        return redirect(url_for("web.match_join", match_id=m.id))
                    if uid not in member_ids or uid in assignments:
                        flash("Invalid member assignment.", "danger")
                        return redirect(url_for("web.match_join", match_id=m.id))
                    grp = next((g for g in slot_groups if (g.get("name") or "") == gname), None)
                    if grp is None or idx < 0 or idx >= int(grp.get("slots", 0)):
                        flash("Invalid slot position.", "danger")
                        return redirect(url_for("web.match_join", match_id=m.id))
                    if MatchSlotParticipant.query.filter_by(match_id=m.id, group_name=gname, slot_index=idx).first():
                        flash("One of the chosen slots is already taken!", "danger")
                        return redirect(url_for("web.match_join", match_id=m.id))
                    assignments[uid] = (gname, idx)
                if len(assignments) != len(member_ids):
                    flash("Place every selected player on a free slot.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))

            # ── Balances: every selected member pays the entry fee ──
            for uid in member_ids:
                w = Wallet.query.filter_by(user_id=uid, kind="deposit").first()
                if m.entry_fee > 0 and (not w or w.balance < m.entry_fee):
                    u = db.session.get(User, uid)
                    flash(f"{(u.display_name if u else 'Player')} has insufficient Entry Wallet balance.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))

            # ── Charge EVERY selected member separately from their own wallet ──
            for uid in member_ids:
                gname, idx = assignments.get(uid, (None, None))
                w = Wallet.query.filter_by(user_id=uid, kind="deposit").first()
                if w and m.entry_fee > 0:
                    w.balance -= m.entry_fee
                    w.last_updated = datetime.utcnow()
                    db.session.add(_entry_tx(uid, w, m.entry_fee))
                p = MatchParticipant(
                    match_id=m.id, user_id=uid, team_id=team.id,
                    status="registered", paid=True,   # fee debited above (or free match)
                    selected_slot_group=gname if slot_groups else None,
                    selected_slot_index=idx,
                )
                db.session.add(p)
                if slot_groups and gname is not None:
                    db.session.add(MatchSlotParticipant(
                        match_id=m.id, user_id=uid, team_id=team.id,
                        group_name=gname, slot_index=idx))
                slot_txt = (f" Slot #{idx+1} ({gname})." if (gname is not None and idx is not None) else "")
                notif = Notification(
                    user_id=uid, type="match_registration",
                    title="Team Joined Match",
                    body=f"Your team [{team.name}] joined {m.title} ({m.game.name})." + slot_txt
                )
                db.session.add(notif)
            db.session.commit()
            flash(f"Team registered! {len(member_ids)} player(s) joined.", "success")
            return redirect(url_for("web.match_join", match_id=m.id))

        elif join_type == "group":
            friend_ids = request.form.getlist("friend_ids", type=int)
            if not friend_ids:
                flash("Please select at least one friend.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            # Check capacity
            if len(friend_ids) + 1 > (m.capacity - m.current_players):
                flash(f"Not enough slots. Only {m.capacity - m.current_players} slot(s) remaining.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            # Get team for THIS game
            from app.models.team import Team, TeamMember
            our_team = Team.query.filter_by(owner_id=user.id, game_id=m.game_id).first()
            if not our_team:
                flash("No team found for this game. Create one first.", "danger")
                return redirect(url_for("web.match_join", match_id=m.id))
            # Verify all selected friends are in the team
            for fid in friend_ids:
                if not TeamMember.query.filter_by(team_id=our_team.id, user_id=fid).first():
                    flash(f"Player #{fid} is not in your team.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
            # Check all balances
            for fid in friend_ids:
                fw = Wallet.query.filter_by(user_id=fid, kind="deposit").first()
                if not fw or fw.balance < m.entry_fee:
                    target = db.session.get(User, fid)
                    flash(f"{target.display_name if target else 'Player'} has insufficient balance.", "danger")
                    return redirect(url_for("web.match_join", match_id=m.id))
            # Register self first
            self_p = MatchParticipant(match_id=m.id, user_id=user.id, status="registered", paid=True)
            db.session.add(self_p)
            # Debit self wallet if fee > 0
            if m.entry_fee > 0 and entry_wallet:
                entry_wallet.balance -= m.entry_fee
                entry_wallet.last_updated = datetime.utcnow()
                db.session.add(_entry_tx(user.id, entry_wallet, m.entry_fee))
            # Register each friend and debit their wallet
            for fid in friend_ids:
                fp = MatchParticipant(match_id=m.id, user_id=fid, status="registered", paid=True)
                db.session.add(fp)
                fw = Wallet.query.filter_by(user_id=fid, kind="deposit").first()
                if fw and m.entry_fee > 0:
                    fw.balance -= m.entry_fee
                    fw.last_updated = datetime.utcnow()
                    db.session.add(_entry_tx(fid, fw, m.entry_fee))
                notif = Notification(user_id=fid, type="match_registration", title="Group Registration", body=f"You joined {m.title} via group registration.")
                db.session.add(notif)
            # Self notification
            notif_self = Notification(user_id=user.id, type="match_registration", title="Group Registration", body=f"You registered group for {m.title} ({len(friend_ids)+1} players total).")
            db.session.add(notif_self)
            db.session.commit()
            flash(f"Group registered! You + {len(friend_ids)} friends joined successfully.", "success")
            return redirect(url_for("web.game_detail", slug=m.game.slug))

    return render_template("webapp/match_confirm.html",
        match=m, user=user,
        already_registered=bool(existing),
        is_registered=bool(existing),
        reg_closed=reg_closed,
        reg_opens_at=reg_opens_at,
        reg_until_start=reg_until_start,
        reg_closed_reason=reg_closed_reason,
        insufficient_balance=insufficient_balance,
        entry_balance=entry_balance,
        currency=currency,
        user_teams=user_teams,
        insufficient_team=insufficient_team_ids,
        team_readiness=team_readiness,
        conflict_match=conflict_match,
        registered_at=registered_at,
        slot_config=slot_config,
        slot_groups=slot_groups,
        occupied_slots=occupied_slots,
        slot_occupants=slot_occupants,
        my_team_members=my_team_members,
        selected_slot_index=participant_slot_index,
        selected_slot_group=participant_slot_group,
        our_team=our_team)


@bp.route("/api/player-search")
@require_login
def api_player_search():
    q = request.args.get("q", "").strip()
    if len(q) < 1:
        # Default: recent users by account creation date (newest first)
        users = User.query.order_by(User.created_at.desc()).limit(15).all()
    else:
        users = User.query.filter(
            (User.username.ilike(f"%{q}%")) | (User.email.ilike(f"%{q}%")) |
            (User.numeric_id.cast(db.String).like(f"%{q}%"))
        ).order_by(User.created_at.desc()).limit(10).all()
    return {"results": [{"id": u.id, "username": u.username or "", "email": u.email,
                         "display_name": u.display_name, "numeric_id": u.numeric_id,
                         "avatar": u.avatar or "", "created_at": u.created_at.strftime('%Y-%m-%d') if u.created_at else ""} for u in users]}


@bp.route("/api/game-modes")
@require_login
def api_game_modes():
    game_id = request.args.get("game_id", type=int)
    if not game_id:
        return {"modes": []}
    modes = GameMode.query.filter_by(game_id=game_id, is_active=True).all()
    return {"modes": [{"id": m.id, "name": m.name, "slug": m.slug} for m in modes]}


@bp.route("/api/wallet/charge", methods=["POST"])
@require_login
def api_wallet_charge():
    from app.services.wallet_service import credit_wallet
    from app.models.wallet import Wallet
    user = db.session.get(User, session["user_id"])
    amount = request.json.get("amount", 0)
    kind = request.json.get("kind", "deposit")
    reason = request.json.get("reason", "")
    if amount <= 0:
        return {"ok": False, "error": "Invalid amount"}
    # payout wallet holds prizes only — users can only charge their entry (deposit) wallet
    if kind != "deposit":
        return {"ok": False, "error": "Only the entry wallet can be charged"}
    w = Wallet.query.filter_by(user_id=user.id, kind=kind).first()
    if not w:
        return {"ok": False, "error": "Wallet not found"}
    try:
        credit_wallet(w.id, amount, reason or "User charge")
        return {"ok": True, "balance": w.balance}
    except Exception as e:
        return {"ok": False, "error": str(e)}


@bp.route("/notifications")
@require_login
def notifications_page():
    """Full page notifications view"""
    from app.models.notification import Notification
    user = db.session.get(User, session["user_id"])
    notifs = Notification.query.filter_by(user_id=user.id).order_by(Notification.created_at.desc()).all()
    return render_template("webapp/notifications.html", user=user, notifications=notifs)


@bp.route("/api/notifications/unread-count")
@require_login
def api_notifications_count():
    """Delegate to the canonical API implementation (single source of truth)."""
    from app.blueprints.api import notif_unread_count
    return notif_unread_count()


@bp.route("/api/notifications")
@require_login
def api_notifications():
    """Delegate to the canonical API implementation (adds icon + formatted time)."""
    from app.blueprints.api import notif_list
    return notif_list()


@bp.route("/api/notifications/<int:nid>/read", methods=["POST"])
@require_login
def api_notification_read(nid):
    """Delegate to the canonical API implementation."""
    from app.blueprints.api import notif_read
    return notif_read(nid)


@bp.route("/api/notifications/read-all", methods=["POST"])
@require_login
def api_notifications_read_all():
    """Delegate to the canonical API implementation."""
    from app.blueprints.api import notif_read_all
    return notif_read_all()


@bp.route("/api/team/invite", methods=["POST"])
@require_login
def api_team_invite():
    """Delegate to the canonical API implementation.

    This route used to DUPLICATE /api/team/invite (api.py) with a stricter
    payload (player_id only) and, being registered first, shadowed the
    canonical handler. Accepts user_id or player_id via the canonical code.
    """
    from app.blueprints.api import team_invite
    return team_invite()


@bp.route("/api/team/invite/<int:iid>/accept", methods=["POST"])
@require_login
def api_team_invite_accept(iid):
    from app.models.team import TeamInvite, TeamMember
    from app.models.notification import Notification
    invite = db.session.get(TeamInvite, iid)
    if not invite or invite.to_user_id != session["user_id"] or invite.status != "pending":
        return {"ok": False, "error": "Invalid invite"}
    existing = TeamMember.query.filter_by(team_id=invite.team_id, user_id=invite.to_user_id).first()
    if not existing:
        member = TeamMember(team_id=invite.team_id, user_id=invite.to_user_id, role="member")
        db.session.add(member)
    invite.status = "accepted"
    # notify the inviter
    if invite.team and invite.team.owner_id:
        notif = Notification(
            user_id=invite.team.owner_id, type="team_accepted",
            title="Team Invitation Accepted",
            body=f"{db.session.get(User, invite.to_user_id).display_name} accepted your invitation and joined {invite.team.name}."
        )
        db.session.add(notif)
    db.session.commit()
    return {"ok": True}


@bp.route("/api/team/invite/<int:iid>/decline", methods=["POST"])
@require_login
def api_team_invite_decline(iid):
    from app.models.team import TeamInvite
    invite = db.session.get(TeamInvite, iid)
    if not invite or invite.to_user_id != session["user_id"] or invite.status != "pending":
        return {"ok": False, "error": "Invalid invite"}
    invite.status = "declined"
    db.session.commit()
    return {"ok": True}


@bp.route("/api/team/invite/<int:iid>/respond", methods=["GET"])
@require_login
def api_team_invite_respond(iid):
    from app.models.team import TeamInvite
    invites = TeamInvite.query.filter_by(to_user_id=session["user_id"], status="pending").all()
    return [{"id": i.id, "team_id": i.team_id, "from_id": i.from_user_id,
             "from_name": i.from_user.display_name if i.from_user else "",
             "team_name": i.team.name if i.team else "",
             "game_name": i.team.game.name if i.team and i.team.game else "",
             "created_at": i.created_at.isoformat()} for i in invites]


@bp.route("/api/team/members/<int:team_id>", methods=["GET"])
@require_login
def api_team_members(team_id):
    from app.models.team import TeamMember
    members = TeamMember.query.filter_by(team_id=team_id).all()
    return [{"id": m.id, "user_id": m.user_id, "role": m.role,
             "display_name": m.user.display_name if m.user else "",
             "numeric_id": m.user.numeric_id if m.user else ""} for m in members]


# ── Tournament Routes ──
@bp.route("/tournaments")
def tournament_list():
    """Public tournament listing page."""
    games = Game.query.filter_by(is_active=True).order_by(Game.sort_order).all()
    # Upcoming & live matches as tournaments
    now = datetime.utcnow()
    upcoming = Match.query.filter(
        Match.status.in_(["upcoming", "live"]),
        Match.starts_at >= now
    ).order_by(Match.starts_at).limit(12).all()

    live = Match.query.filter(Match.status == "live").order_by(Match.starts_at).limit(8).all()

    # Recently finished tournaments with announced winners (results section)
    finished = Match.query.filter(
        Match.status == "finished",
        Match.winner_participant_ids.isnot(None),
        Match.winner_participant_ids != "[]"
    ).order_by(Match.starts_at.desc()).limit(8).all()

    # Precompute winner users per finished match for card display
    import json
    from app.models.match import MatchParticipant
    finished_results = []
    for fm in finished:
        try:
            wids = [int(x) for x in json.loads(fm.winner_participant_ids or "[]")]
        except Exception:
            wids = []
        winners = []
        if wids:
            parts = MatchParticipant.query.filter(MatchParticipant.id.in_(wids)).all()
            by_id = {p.id: p for p in parts}
            winners = [by_id[w].user for w in wids if w in by_id and by_id[w].user]
        finished_results.append({"match": fm, "winners": winners})

    featured = Match.query.filter_by(is_special=True, status="upcoming").order_by(Match.starts_at).first()

    # Platform stats strip (like Battlefy / Challengermode landing stats)
    from sqlalchemy import func as _f
    open_pool = (db.session.query(_f.coalesce(_f.sum(Match.prize_pool), 0))
                 .filter(Match.status.in_(["upcoming", "live"])).scalar()) or 0
    open_count = Match.query.filter(Match.status.in_(["upcoming", "live"])).count()
    registered_players = (db.session.query(_f.count(db.distinct(MatchParticipant.user_id)))
                          .join(Match).filter(Match.status.in_(["upcoming", "live"])).scalar()) or 0

    return render_template("webapp/tournaments.html", games=games,
                           upcoming_matches=upcoming, live_matches=live,
                           finished_matches=finished, finished_results=finished_results,
                           featured_match=featured,
                           open_pool=int(open_pool), open_count=open_count,
                           registered_players=registered_players)


# ── Results History (تاریخچه نتایج) ──
@bp.route("/results")
def results_page():
    """Dedicated results-history page: every finished tournament with
    announced winners, per-rank prizes, and a Hall of Champions."""
    import json as _json
    from app.models.match import MatchParticipant
    from app.models.leaderboard import PointEvent

    games = Game.query.filter_by(is_active=True).order_by(Game.sort_order).all()
    sel_game = None
    q = (Match.query
         .filter(Match.status == "finished",
                 Match.winner_participant_ids.isnot(None),
                 Match.winner_participant_ids != "[]"))
    sel_slug = request.args.get("game")
    sel_game = None
    if sel_slug:
        sel_game = Game.query.filter_by(slug=sel_slug).first()
        if sel_game:
            q = q.filter(Match.game_id == sel_game.id)
            finished = q.order_by(Match.starts_at.desc()).limit(60).all()
        else:
            finished = []   # unknown game slug → empty result list
    else:
        finished = q.order_by(Match.starts_at.desc()).limit(60).all()

    results, champions = [], {}
    total_prize = 0.0
    for fm in finished:
        try:
            wid_list = [int(x) for x in _json.loads(fm.winner_participant_ids or "[]")]
        except Exception:
            wid_list = []
        parts = (MatchParticipant.query.filter(MatchParticipant.id.in_(wid_list)).all()
                 if wid_list else [])
        by_id = {p.id: p for p in parts}
        evs = PointEvent.query.filter_by(match_id=fm.id).all()
        prize_by_uid = {}
        for ev in evs:
            if ev.prize and ev.prize > 0:
                prize_by_uid[ev.user_id] = float(ev.prize)
        winners = []
        for w in wid_list:
            p = by_id.get(w)
            if not p:
                continue
            prize = prize_by_uid.get(p.user_id, 0.0)
            winners.append({"user": p.user, "prize": prize})
            if p.user:
                ch = champions.setdefault(p.user.id, {"user": p.user, "titles": 0, "prize": 0.0})
                ch["titles"] += 1
                ch["prize"] += prize
        total_prize += sum(w["prize"] for w in winners)
        results.append({"match": fm, "winners": winners})

    hall = sorted(champions.values(), key=lambda c: (c["titles"], c["prize"]), reverse=True)[:10]
    return render_template("webapp/results.html", games=games, sel_game=sel_game,
                           results=results, hall=hall, total_prize=total_prize,
                           total_tournaments=len(results))


# ── Public player profile (FACEIT-style stats page) ──
@bp.route("/player/<int:numeric_id>")
def player_profile(numeric_id):
    from app.models.leaderboard import PointEvent
    p_user = User.query.filter_by(numeric_id=numeric_id).first()
    if not p_user:
        abort(404)

    agg = (db.session.query(
        db.func.coalesce(db.func.sum(PointEvent.points), 0).label("points"),
        db.func.count(PointEvent.id).label("matches"),
        db.func.sum(db.case((PointEvent.kind == "win", 1), else_=0)).label("wins"),
        db.func.coalesce(db.func.sum(PointEvent.prize), 0.0).label("prize"))
        .filter(PointEvent.user_id == p_user.id).first())
    matches = int(agg.matches or 0)
    wins = int(agg.wins or 0)
    win_rate = round((wins / matches) * 100, 1) if matches else 0.0

    per_game = (db.session.query(
        Game,
        db.func.sum(PointEvent.points).label("pts"),
        db.func.count(PointEvent.id).label("ms"),
        db.func.sum(db.case((PointEvent.kind == "win", 1), else_=0)).label("wn"))
        .join(PointEvent, PointEvent.game_id == Game.id)
        .filter(PointEvent.user_id == p_user.id)
        .group_by(Game.id)
        .order_by(db.desc("pts")).limit(4).all())

    trophies = (PointEvent.query.filter_by(user_id=p_user.id, kind="win")
                .order_by(PointEvent.created_at.desc()).limit(12).all())
    recent = (PointEvent.query.filter_by(user_id=p_user.id)
              .order_by(PointEvent.created_at.desc()).limit(10).all())

    return render_template("webapp/player.html", p_user=p_user,
                           points=int(agg.points), matches=matches, wins=wins,
                           win_rate=win_rate, total_prize=float(agg.prize or 0),
                           per_game=per_game, trophies=trophies, recent=recent)


@bp.route("/tournament/<int:match_id>")
def tournament_detail(match_id):
    """Tournament detail page with bracket."""
    from app.models.match import MatchParticipant
    m = db.session.get(Match, match_id)
    if not m:
        abort(404)

    is_registered = False
    user = None
    if "user_id" in session:
        user = db.session.get(User, session["user_id"])
        existing = MatchParticipant.query.filter_by(match_id=m.id, user_id=user.id).first()
        if existing:
            is_registered = True

    participants = MatchParticipant.query.filter_by(match_id=m.id, status="registered").all()
    capacity = m.capacity if m.capacity else len(participants)

    # ── Winner announcement (set by admin award) ─────────────────────
    import json as _json
    from app.models.leaderboard import PointEvent
    try:
        wids = [int(x) for x in _json.loads(m.winner_participant_ids or "[]")]
    except Exception:
        wids = []
    winners_announced = bool(wids) and m.status == "finished"
    winner_rows, match_points, match_prizes = [], {}, {}
    if winners_announced:
        by_id = {p.id: p for p in m.participants.all()}
        winner_rows = [by_id[w] for w in wids if w in by_id]
        for ev in PointEvent.query.filter_by(match_id=m.id).all():
            match_points[ev.user_id] = max(match_points.get(ev.user_id, 0), ev.points)
            if ev.prize and ev.prize > 0:
                match_prizes[ev.user_id] = float(ev.prize)

    return render_template("webapp/tournament_detail.html", match=m,
                           participants=participants, capacity=capacity,
                           is_registered=is_registered, user=user,
                           winners_announced=winners_announced,
                           winner_rows=winner_rows, match_points=match_points,
                           match_prizes=match_prizes,
                           reg_closed_reason=("started" if datetime.utcnow() >= m.starts_at else ("status" if m.status in ("finished", "cancelled", "live") else "not_open_yet")) if m.registration_closed else "",
                           reg_opens_at=m.registration_opens_at)


@bp.route("/tournament/<int:match_id>/lobby")
def tournament_lobby(match_id):
    """Tournament teams & slots lobby — mobile-first esports layout.

    Dynamically groups registered participants into team sections based on
    the tournament's slot configuration. Falls back to a simple sequential
    grouping (teams of squad_size) when slot_layout is empty.
    """
    from app.models.match import MatchParticipant, MatchSlotParticipant
    from app.models.team import Team

    m = db.session.get(Match, match_id)
    if not m:
        abort(404)

    user = db.session.get(User, session["user_id"]) if "user_id" in session else None

    # All registered participants (status registered/confirmed)
    participants = (MatchParticipant.query
        .filter_by(match_id=m.id)
        .filter(MatchParticipant.status.in_(["registered", "confirmed"]))
        .order_by(MatchParticipant.registered_at)
        .all())

    # Slot config: groups define named columns of slots
    slot_groups = m.slot_config.get("groups", []) if m.slot_config else []

    # Build teams dynamically from registrations
    teams = []

    if m.team_mode and m.mode and m.mode.squad_size:
        squad = m.mode.squad_size
        # Group participants by their team_id
        team_map = {}
        solo_participants = []
        for p in participants:
            if p.team_id:
                team = Team.query.get(p.team_id)
                if team:
                    team_map.setdefault(team.id, {"team": team, "players": [], "team_obj": team})
                    # Resolve the user object
                    u = db.session.get(User, p.user_id)
                    team_map[team.id]["players"].append(u)
                    continue
            solo_participants.append(p)

        team_num = 1
        team_colors = ['team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8',
                       'team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8']

        # Render existing teams
        for tkey in sorted(team_map.keys()):
            tdata = team_map[tkey]
            team_obj = tdata["team_obj"]
            players = tdata["players"]
            total_slots = squad
            filled = len(players)

            # Build slot list: real players + empty placeholders
            slot_players = players[:total_slots]
            empty_count = total_slots - filled
            empty_slots = [{"empty": True}] * empty_count

            all_slots = [{"player": p, "empty": False} for p in slot_players] + empty_slots

            teams.append({
                "team_obj": team_obj,
                "team_number": team_num,
                "team_color": team_colors[team_num - 1] if team_num <= len(team_colors) else 'team-1',
                "team_name": team_obj.name if team_obj else f"Team {team_num:02d}",
                "team_logo": team_obj.logo if team_obj and hasattr(team_obj, 'logo') else None,
                "filled": filled,
                "total": total_slots,
                "slots": all_slots,
            })
            team_num += 1

        # Render solo participants as placeholder teams
        for p in solo_participants:
            u = db.session.get(User, p.user_id)
            if u:
                all_slots = [{"player": u, "empty": False}]
                # Fill remaining slots for the squad
                for _ in range(squad - 1):
                    all_slots.append({"empty": True})

                teams.append({
                    "team_obj": None,
                    "team_number": team_num,
                    "team_color": team_colors[team_num - 1] if team_num <= len(team_colors) else 'team-1',
                    "team_name": u.display_name or u.username,
                    "team_logo": u.avatar,
                    "filled": 1,
                    "total": squad,
                    "slots": all_slots,
                })
                team_num += 1

    elif slot_groups:
        # Multi-group slot layout (e.g., City / Normal groups)
        for grp in slot_groups:
            gname = grp.get("name", "Group")
            gslots = int(grp.get("slots", 0))
            gpps = int(grp.get("players_per_slot", 1))

            # Get slot participants for this group
            slot_map = {}
            sps = MatchSlotParticipant.query.filter_by(match_id=m.id, group_name=gname).all()
            for sp in sps:
                slot_map.setdefault(sp.slot_index, []).append(sp)

            team_num = 1
            team_colors_cycle = ['team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8']

            for si in range(gslots):
                occupants = slot_map.get(si, [])
                total_slots = gpps
                filled = len(occupants)
                all_slots = []
                for sp in occupants:
                    u = db.session.get(User, sp.user_id)
                    all_slots.append({"player": u, "empty": False})
                empty_count = total_slots - filled
                for _ in range(empty_count):
                    all_slots.append({"empty": True})

                teams.append({
                    "team_obj": None,
                    "team_number": team_num,
                    "team_color": team_colors_cycle[(team_num - 1) % len(team_colors_cycle)],
                    "team_name": f"{gname} — Slot {si + 1}",
                    "team_logo": None,
                    "filled": filled,
                    "total": total_slots,
                    "slots": all_slots,
                })
                team_num += 1

    else:
        # Fallback: sequential grouping into teams of squad_size
        squad = m.mode.squad_size if m.mode and m.mode.squad_size else 1
        team_num = 1
        team_colors = ['team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8',
                       'team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8']

        for i in range(0, len(participants), squad):
            chunk = participants[i:i+squad]
            all_slots = []
            for p in chunk:
                u = db.session.get(User, p.user_id)
                all_slots.append({"player": u, "empty": False})
            empty_count = squad - len(chunk)
            for _ in range(empty_count):
                all_slots.append({"empty": True})

            teams.append({
                "team_obj": None,
                "team_number": team_num,
                "team_color": team_colors[team_num - 1] if team_num <= len(team_colors) else 'team-1',
                "team_name": f"Team {team_num:02d}",
                "team_logo": None,
                "filled": len(chunk),
                "total": squad,
                "slots": all_slots,
            })
            team_num += 1

    total_capacity = m.capacity if m.capacity else (len(teams) * (m.mode.squad_size if m.mode else 1))
    total_registered = len(participants)
    total_teams = len(teams)
    slot_limit = total_capacity
    if m.mode and m.mode.squad_size and not m.team_mode:
        slot_limit = total_capacity

    return render_template("webapp/tournament_lobby.html",
                           match=m, match_id=m.id,
                           teams=teams,
                           total_registered=total_registered,
                           total_capacity=total_capacity,
                           total_teams=total_teams,
                           slot_limit=slot_limit,
                           is_registered=bool(user and MatchParticipant.query.filter_by(match_id=m.id, user_id=user.id).first()) if user else False,
                           user=user,
                           squad_size=m.mode.squad_size if m.mode else None)


@bp.route("/rewards")
def rewards_page():
    """Rewards and XP system page."""
    user = None
    if "user_id" in session:
        user = db.session.get(User, session["user_id"])

    return render_template("webapp/rewards.html", user=user)


@bp.route("/how-it-works")
def how_it_works():
    """How it works landing page."""
    return render_template("webapp/how_it_works.html")


@bp.route("/api/tournament/stats")
def api_tournament_stats():
    """Live stats API for hero section."""
    from app.models.match import Match
    now = datetime.utcnow()
    players_online = 0
    for m in Match.query.filter(Match.status == "live").all():
        players_online += m.current_players

    live_count = Match.query.filter(Match.status == "live").count()
    upcoming = Match.query.filter(
        Match.status.in_(["upcoming", "live"]),
        Match.starts_at >= now
    ).order_by(Match.starts_at).first()

    next_match = None
    if upcoming:
        next_match = {
            "title": upcoming.title,
            "game": upcoming.game.name if upcoming.game else "",
            "starts_at": upcoming.starts_at.isoformat() if upcoming.starts_at else None,
        }

    return {"players_online": players_online, "live_tournaments": live_count, "next_match": next_match}


@bp.route("/api/tournament/<int:match_id>/lobby")
def api_tournament_lobby(match_id):
    """JSON endpoint for real-time tournament lobby updates."""
    from app.models.match import MatchParticipant, MatchSlotParticipant
    from app.models.team import Team

    m = db.session.get(Match, match_id)
    if not m:
        return {"error": "not found"}, 404

    user = db.session.get(User, session["user_id"]) if "user_id" in session else None

    participants = (MatchParticipant.query
        .filter_by(match_id=m.id)
        .filter(MatchParticipant.status.in_(["registered", "confirmed"]))
        .order_by(MatchParticipant.registered_at)
        .all())

    slot_groups = m.slot_config.get("groups", []) if m.slot_config else []
    teams = []

    if m.team_mode and m.mode and m.mode.squad_size:
        squad = m.mode.squad_size
        team_map = {}
        solo_participants = []
        for p in participants:
            if p.team_id:
                team = Team.query.get(p.team_id)
                if team:
                    team_map.setdefault(team.id, {"team_id": team.id, "team_name": team.name, "logo": team.logo, "players": [], "captain_id": team.owner_id if team else None})
                    u = db.session.get(User, p.user_id)
                    if u:
                        team_map[team.id]["players"].append({
                            "id": u.id, "username": u.username, "display_name": u.display_name,
                            "avatar": u.avatar, "numeric_id": u.numeric_id,
                            "is_captain": u.id == team_map[team.id]["captain_id"]
                        })
                    continue
            solo_participants.append(p)

        team_num = 1
        team_colors = ['team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8']

        for tkey in sorted(team_map.keys()):
            tdata = team_map[tkey]
            players = tdata["players"]
            filled = len(players)
            all_slots = [{"player": p, "empty": False} for p in players] + [{"empty": True}] * (squad - filled)
            teams.append({
                "team_number": team_num,
                "team_color": team_colors[(team_num - 1) % len(team_colors)],
                "team_name": tdata["team_name"],
                "team_logo": tdata.get("logo"),
                "captain_username": None,
                "filled": filled,
                "total": squad,
                "slots": all_slots,
            })
            team_num += 1

        for p in solo_participants:
            u = db.session.get(User, p.user_id)
            if u:
                all_slots = [{"player": {"id": u.id, "username": u.username, "display_name": u.display_name, "avatar": u.avatar, "numeric_id": u.numeric_id, "is_captain": False}, "empty": False}] + [{"empty": True}] * (squad - 1)
                teams.append({
                    "team_number": team_num,
                    "team_color": team_colors[(team_num - 1) % len(team_colors)],
                    "team_name": u.display_name or u.username,
                    "team_logo": u.avatar,
                    "captain_username": u.username,
                    "filled": 1,
                    "total": squad,
                    "slots": all_slots,
                })
                team_num += 1

    elif slot_groups:
        for grp in slot_groups:
            gname = grp.get("name", "Group")
            gslots = int(grp.get("slots", 0))
            gpps = int(grp.get("players_per_slot", 1))
            slot_map = {}
            sps = MatchSlotParticipant.query.filter_by(match_id=m.id, group_name=gname).all()
            for sp in sps:
                slot_map.setdefault(sp.slot_index, []).append(sp)

            team_num = 1
            team_colors = ['team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8']
            for si in range(gslots):
                occupants = slot_map.get(si, [])
                filled = len(occupants)
                all_slots = []
                for sp in occupants:
                    u = db.session.get(User, sp.user_id)
                    if u:
                        all_slots.append({"player": {"id": u.id, "username": u.username, "display_name": u.display_name, "avatar": u.avatar, "numeric_id": u.numeric_id, "is_captain": False}, "empty": False})
                for _ in range(gpps - filled):
                    all_slots.append({"empty": True})
                teams.append({
                    "team_number": team_num,
                    "team_color": team_colors[(team_num - 1) % len(team_colors)],
                    "team_name": f"{gname} — Slot {si + 1}",
                    "team_logo": None,
                    "captain_username": None,
                    "filled": filled,
                    "total": gpps,
                    "slots": all_slots,
                })
                team_num += 1
    else:
        squad = m.mode.squad_size if m.mode and m.mode.squad_size else 1
        team_num = 1
        team_colors = ['team-1','team-2','team-3','team-4','team-5','team-6','team-7','team-8']
        for i in range(0, len(participants), squad):
            chunk = participants[i:i+squad]
            all_slots = []
            for p in chunk:
                u = db.session.get(User, p.user_id)
                if u:
                    all_slots.append({"player": {"id": u.id, "username": u.username, "display_name": u.display_name, "avatar": u.avatar, "numeric_id": u.numeric_id, "is_captain": False}, "empty": False})
            for _ in range(squad - len(chunk)):
                all_slots.append({"empty": True})
            teams.append({
                "team_number": team_num,
                "team_color": team_colors[(team_num - 1) % len(team_colors)],
                "team_name": f"Team {team_num:02d}",
                "team_logo": None,
                "captain_username": None,
                "filled": len(chunk),
                "total": squad,
                "slots": all_slots,
            })
            team_num += 1

    return {
        "match": {
            "id": m.id,
            "title": m.title,
            "status": m.status,
            "entry_fee": m.entry_fee,
            "prize_pool": m.prize_pool,
            "starts_at": m.starts_at.isoformat() if m.starts_at else None,
            "is_team_mode": m.team_mode,
            "game_name": m.game.name if m.game else "",
            "game_icon": m.game.icon if m.game else "",
        },
        "teams": teams,
        "total_registered": len(participants),
        "total_capacity": m.capacity if m.capacity else len(teams) * (m.mode.squad_size if m.mode else 1),
        "total_teams": len(teams),
        "is_registered": bool(user and MatchParticipant.query.filter_by(match_id=m.id, user_id=user.id).first()) if user else False,
    }
