from datetime import timedelta
import secrets
import string
from fastapi import APIRouter, HTTPException, status, Depends, UploadFile, File, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload

from app.api.deps import (
    DbSession, CurrentUser,
    require_participant, require_organizer, require_creator,
    get_participant,
)
from app.core.config import settings
from app.core.security import decode_access_token
from app.services.storage import storage_service

# Optional auth for endpoints that need it conditionally
optional_auth = HTTPBearer(auto_error=False)
from app.models import (
    Marathon, Participant, MarathonStatus, Game, GameStatus, Challenge,
    Assignment, AssignmentStatus, Activity, ActivityType, ParticipantRole,
    Dispute, DisputeStatus, BonusAssignment, BonusAssignmentStatus, User,
    ExiledGame,
)
from app.schemas import (
    MarathonCreate,
    MarathonUpdate,
    MarathonResponse,
    MarathonListItem,
    MarathonPublicInfo,
    JoinMarathon,
    ParticipantInfo,
    ParticipantWithUser,
    LeaderboardEntry,
    MessageResponse,
    UserPublic,
    SetParticipantRole,
    OrganizerSkipRequest,
    ExiledGameResponse,
)
from app.services.telegram_notifier import telegram_notifier

router = APIRouter(prefix="/marathons", tags=["marathons"])


# Public endpoint (no auth required)
@router.get("/by-code/{invite_code}", response_model=MarathonPublicInfo)
async def get_marathon_by_code(invite_code: str, db: DbSession):
    """Get public marathon info by invite code. No authentication required."""
    result = await db.execute(
        select(Marathon, func.count(Participant.id).label("participants_count"))
        .outerjoin(Participant)
        .options(selectinload(Marathon.creator))
        .where(func.upper(Marathon.invite_code) == invite_code.upper())
        .group_by(Marathon.id)
    )
    row = result.first()

    if not row:
        raise HTTPException(status_code=404, detail="Marathon not found")

    marathon = row[0]
    participants_count = row[1]

    return MarathonPublicInfo(
        id=marathon.id,
        title=marathon.title,
        description=marathon.description,
        status=marathon.status,
        cover_url=marathon.cover_url,
        participants_count=participants_count,
        creator_nickname=marathon.creator.nickname,
    )


def generate_invite_code() -> str:
    """Generate a clean 8-character uppercase alphanumeric code."""
    alphabet = string.ascii_uppercase + string.digits
    return ''.join(secrets.choice(alphabet) for _ in range(8))


async def get_marathon_or_404(db, marathon_id: int) -> Marathon:
    result = await db.execute(
        select(Marathon)
        .options(
            selectinload(Marathon.creator).selectinload(User.equipped_frame),
            selectinload(Marathon.creator).selectinload(User.equipped_title),
            selectinload(Marathon.creator).selectinload(User.equipped_name_color),
            selectinload(Marathon.creator).selectinload(User.equipped_background),
        )
        .where(Marathon.id == marathon_id)
    )
    marathon = result.scalar_one_or_none()
    if not marathon:
        raise HTTPException(status_code=404, detail="Marathon not found")
    return marathon


async def get_participation(db, user_id: int, marathon_id: int) -> Participant | None:
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == user_id,
            Participant.marathon_id == marathon_id,
        )
    )
    return result.scalar_one_or_none()


@router.get("", response_model=list[MarathonListItem])
async def list_marathons(current_user: CurrentUser, db: DbSession):
    """Get all marathons where user is participant, creator, or public marathons"""
    # Admin can see all marathons
    if current_user.is_admin:
        result = await db.execute(
            select(Marathon, func.count(Participant.id).label("participants_count"))
            .outerjoin(Participant)
            .group_by(Marathon.id)
            .order_by(Marathon.created_at.desc())
        )
    else:
        # User can see: own marathons, participated marathons, and public marathons
        result = await db.execute(
            select(Marathon, func.count(Participant.id).label("participants_count"))
            .outerjoin(Participant)
            .where(
                (Marathon.creator_id == current_user.id) |
                (Participant.user_id == current_user.id) |
                (Marathon.is_public == True)
            )
            .group_by(Marathon.id)
            .order_by(Marathon.created_at.desc())
        )

    marathons = []
    for row in result.all():
        marathon = row[0]
        marathons.append(MarathonListItem(
            id=marathon.id,
            title=marathon.title,
            status=marathon.status,
            is_public=marathon.is_public,
            cover_url=marathon.cover_url,
            participants_count=row[1],
            start_date=marathon.start_date,
            end_date=marathon.end_date,
        ))

    return marathons


@router.post("", response_model=MarathonResponse)
async def create_marathon(
    data: MarathonCreate,
    current_user: CurrentUser,
    db: DbSession,
):
    # Strip timezone info for naive datetime columns
    start_date = data.start_date.replace(tzinfo=None) if data.start_date.tzinfo else data.start_date
    end_date = start_date + timedelta(days=data.duration_days)

    marathon = Marathon(
        title=data.title,
        description=data.description,
        creator_id=current_user.id,
        invite_code=generate_invite_code(),
        is_public=data.is_public,
        game_proposal_mode=data.game_proposal_mode,
        start_date=start_date,
        end_date=end_date,
    )
    db.add(marathon)
    await db.flush()

    # Auto-add creator as organizer participant
    participant = Participant(
        user_id=current_user.id,
        marathon_id=marathon.id,
        role=ParticipantRole.ORGANIZER.value,  # Creator is organizer
    )
    db.add(participant)

    await db.commit()
    await db.refresh(marathon)

    return MarathonResponse(
        id=marathon.id,
        title=marathon.title,
        description=marathon.description,
        creator=UserPublic.model_validate(current_user),
        status=marathon.status,
        invite_code=marathon.invite_code,
        is_public=marathon.is_public,
        game_proposal_mode=marathon.game_proposal_mode,
        auto_events_enabled=marathon.auto_events_enabled,
        cover_url=marathon.cover_url,
        start_date=marathon.start_date,
        end_date=marathon.end_date,
        participants_count=1,
        games_count=0,
        created_at=marathon.created_at,
        my_participation=ParticipantInfo.model_validate(participant),
    )


@router.get("/{marathon_id}", response_model=MarathonResponse)
async def get_marathon(marathon_id: int, current_user: CurrentUser, db: DbSession):
    marathon = await get_marathon_or_404(db, marathon_id)

    # For private marathons, require participation (or admin/creator)
    if not marathon.is_public and not current_user.is_admin:
        participation = await get_participation(db, current_user.id, marathon_id)
        if not participation:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="You are not a participant of this private marathon",
            )

    # Count participants and approved games
    participants_count = await db.scalar(
        select(func.count()).select_from(Participant).where(Participant.marathon_id == marathon_id)
    )
    games_count = await db.scalar(
        select(func.count()).select_from(Game).where(
            Game.marathon_id == marathon_id,
            Game.status == GameStatus.APPROVED.value,
        )
    )

    # Get user's participation
    participation = await get_participation(db, current_user.id, marathon_id)

    return MarathonResponse(
        id=marathon.id,
        title=marathon.title,
        description=marathon.description,
        creator=UserPublic.model_validate(marathon.creator),
        status=marathon.status,
        invite_code=marathon.invite_code,
        is_public=marathon.is_public,
        game_proposal_mode=marathon.game_proposal_mode,
        auto_events_enabled=marathon.auto_events_enabled,
        cover_url=marathon.cover_url,
        start_date=marathon.start_date,
        end_date=marathon.end_date,
        participants_count=participants_count,
        games_count=games_count,
        created_at=marathon.created_at,
        my_participation=ParticipantInfo.model_validate(participation) if participation else None,
    )


@router.patch("/{marathon_id}", response_model=MarathonResponse)
async def update_marathon(
    marathon_id: int,
    data: MarathonUpdate,
    current_user: CurrentUser,
    db: DbSession,
):
    # Require organizer role
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    if marathon.status != MarathonStatus.PREPARING.value:
        raise HTTPException(status_code=400, detail="Cannot update active or finished marathon")

    if data.title is not None:
        marathon.title = data.title
    if data.description is not None:
        marathon.description = data.description
    if data.start_date is not None:
        # Strip timezone info for naive datetime columns
        marathon.start_date = data.start_date.replace(tzinfo=None) if data.start_date.tzinfo else data.start_date
    if data.is_public is not None:
        marathon.is_public = data.is_public
    if data.game_proposal_mode is not None:
        marathon.game_proposal_mode = data.game_proposal_mode
    if data.auto_events_enabled is not None:
        marathon.auto_events_enabled = data.auto_events_enabled

    await db.commit()

    return await get_marathon(marathon_id, current_user, db)


@router.delete("/{marathon_id}", response_model=MessageResponse)
async def delete_marathon(marathon_id: int, current_user: CurrentUser, db: DbSession):
    # Only creator or admin can delete
    await require_creator(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    await db.delete(marathon)
    await db.commit()

    return MessageResponse(message="Marathon deleted")


@router.post("/{marathon_id}/start", response_model=MarathonResponse)
async def start_marathon(marathon_id: int, current_user: CurrentUser, db: DbSession):
    # Require organizer role
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    if marathon.status != MarathonStatus.PREPARING.value:
        raise HTTPException(status_code=400, detail="Marathon is not in preparing state")

    # Check if there are approved games
    games_result = await db.execute(
        select(Game).where(
            Game.marathon_id == marathon_id,
            Game.status == GameStatus.APPROVED.value,
        )
    )
    approved_games = games_result.scalars().all()

    if len(approved_games) == 0:
        raise HTTPException(status_code=400, detail="Добавьте и одобрите хотя бы одну игру")

    # Check that all approved challenge-based games have at least one challenge
    # Playthrough games don't need challenges
    games_without_challenges = []
    for game in approved_games:
        if game.is_playthrough:
            continue  # Игры типа "Прохождение" не требуют челленджей
        challenge_count = await db.scalar(
            select(func.count()).select_from(Challenge).where(Challenge.game_id == game.id)
        )
        if challenge_count == 0:
            games_without_challenges.append(game.title)

    if games_without_challenges:
        games_list = ", ".join(games_without_challenges)
        raise HTTPException(
            status_code=400,
            detail=f"У следующих игр нет челленджей: {games_list}"
        )

    marathon.status = MarathonStatus.ACTIVE.value

    # Log activity
    activity = Activity(
        marathon_id=marathon_id,
        user_id=current_user.id,
        type=ActivityType.START_MARATHON.value,
        data={"title": marathon.title},
    )
    db.add(activity)

    await db.commit()

    # Send Telegram notifications
    await telegram_notifier.notify_marathon_start(db, marathon_id, marathon.title)

    return await get_marathon(marathon_id, current_user, db)


@router.post("/{marathon_id}/finish", response_model=MarathonResponse)
async def finish_marathon(marathon_id: int, current_user: CurrentUser, db: DbSession):
    from app.services.coins import coins_service

    # Require organizer role
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    if marathon.status != MarathonStatus.ACTIVE.value:
        raise HTTPException(status_code=400, detail="Marathon is not active")

    marathon.status = MarathonStatus.FINISHED.value

    # Award coins for top 3 places (only in certified marathons)
    if marathon.is_certified:
        # Get top 3 participants by total_points
        top_result = await db.execute(
            select(Participant)
            .options(selectinload(Participant.user))
            .where(Participant.marathon_id == marathon_id)
            .order_by(Participant.total_points.desc())
            .limit(3)
        )
        top_participants = top_result.scalars().all()

        for place, participant in enumerate(top_participants, start=1):
            if participant.total_points > 0:  # Only award if they have points
                await coins_service.award_marathon_place(
                    db, participant.user, marathon, place
                )

    # Log activity
    activity = Activity(
        marathon_id=marathon_id,
        user_id=current_user.id,
        type=ActivityType.FINISH_MARATHON.value,
        data={"title": marathon.title},
    )
    db.add(activity)

    await db.commit()

    # Send Telegram notifications
    await telegram_notifier.notify_marathon_finish(db, marathon_id, marathon.title)

    return await get_marathon(marathon_id, current_user, db)


@router.post("/join", response_model=MarathonResponse)
async def join_marathon(data: JoinMarathon, current_user: CurrentUser, db: DbSession):
    result = await db.execute(
        select(Marathon).where(func.upper(Marathon.invite_code) == data.invite_code.upper())
    )
    marathon = result.scalar_one_or_none()

    if not marathon:
        raise HTTPException(status_code=404, detail="Invalid invite code")

    if marathon.status == MarathonStatus.FINISHED.value:
        raise HTTPException(status_code=400, detail="Marathon has already finished")

    # Check if already participant
    existing = await get_participation(db, current_user.id, marathon.id)
    if existing:
        raise HTTPException(status_code=400, detail="Already joined this marathon")

    participant = Participant(
        user_id=current_user.id,
        marathon_id=marathon.id,
        role=ParticipantRole.PARTICIPANT.value,  # Regular participant
    )
    db.add(participant)

    # Log activity
    activity = Activity(
        marathon_id=marathon.id,
        user_id=current_user.id,
        type=ActivityType.JOIN.value,
        data={"nickname": current_user.nickname},
    )
    db.add(activity)

    await db.commit()

    return await get_marathon(marathon.id, current_user, db)


@router.post("/{marathon_id}/join", response_model=MarathonResponse)
async def join_public_marathon(marathon_id: int, current_user: CurrentUser, db: DbSession):
    """Join a public marathon without invite code"""
    marathon = await get_marathon_or_404(db, marathon_id)

    if not marathon.is_public:
        raise HTTPException(status_code=403, detail="This marathon is private. Use invite code to join.")

    if marathon.status == MarathonStatus.FINISHED.value:
        raise HTTPException(status_code=400, detail="Marathon has already finished")

    # Check if already participant
    existing = await get_participation(db, current_user.id, marathon.id)
    if existing:
        raise HTTPException(status_code=400, detail="Already joined this marathon")

    participant = Participant(
        user_id=current_user.id,
        marathon_id=marathon.id,
        role=ParticipantRole.PARTICIPANT.value,
    )
    db.add(participant)

    # Log activity
    activity = Activity(
        marathon_id=marathon.id,
        user_id=current_user.id,
        type=ActivityType.JOIN.value,
        data={"nickname": current_user.nickname},
    )
    db.add(activity)

    await db.commit()

    return await get_marathon(marathon.id, current_user, db)


@router.get("/{marathon_id}/participants", response_model=list[ParticipantWithUser])
async def get_participants(marathon_id: int, current_user: CurrentUser, db: DbSession):
    marathon = await get_marathon_or_404(db, marathon_id)

    # For private marathons, require participation (or admin)
    if not marathon.is_public and not current_user.is_admin:
        participation = await get_participation(db, current_user.id, marathon_id)
        if not participation:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="You are not a participant of this private marathon",
            )

    result = await db.execute(
        select(Participant)
        .options(
            selectinload(Participant.user).selectinload(User.equipped_frame),
            selectinload(Participant.user).selectinload(User.equipped_title),
            selectinload(Participant.user).selectinload(User.equipped_name_color),
            selectinload(Participant.user).selectinload(User.equipped_background),
        )
        .where(Participant.marathon_id == marathon_id)
        .order_by(Participant.joined_at)
    )
    participants = result.scalars().all()

    return [
        ParticipantWithUser(
            id=p.id,
            role=p.role,
            total_points=p.total_points,
            current_streak=p.current_streak,
            drop_count=p.drop_count,
            joined_at=p.joined_at,
            user=UserPublic.model_validate(p.user),
        )
        for p in participants
    ]


@router.patch("/{marathon_id}/participants/{user_id}/role", response_model=ParticipantWithUser)
async def set_participant_role(
    marathon_id: int,
    user_id: int,
    data: SetParticipantRole,
    current_user: CurrentUser,
    db: DbSession,
):
    """Set participant's role (only creator can do this)"""
    # Only creator can change roles
    marathon = await require_creator(db, current_user, marathon_id)

    # Cannot change creator's role
    if user_id == marathon.creator_id:
        raise HTTPException(status_code=400, detail="Cannot change creator's role")

    # Get participant
    result = await db.execute(
        select(Participant)
        .options(
            selectinload(Participant.user).selectinload(User.equipped_frame),
            selectinload(Participant.user).selectinload(User.equipped_title),
            selectinload(Participant.user).selectinload(User.equipped_name_color),
            selectinload(Participant.user).selectinload(User.equipped_background),
        )
        .where(
            Participant.marathon_id == marathon_id,
            Participant.user_id == user_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=404, detail="Participant not found")

    participant.role = data.role
    await db.commit()
    await db.refresh(participant)

    return ParticipantWithUser(
        id=participant.id,
        role=participant.role,
        total_points=participant.total_points,
        current_streak=participant.current_streak,
        drop_count=participant.drop_count,
        joined_at=participant.joined_at,
        user=UserPublic.model_validate(participant.user),
    )


@router.get("/{marathon_id}/leaderboard", response_model=list[LeaderboardEntry])
async def get_leaderboard(
    marathon_id: int,
    db: DbSession,
    credentials: HTTPAuthorizationCredentials | None = Depends(optional_auth),
):
    """
    Get marathon leaderboard.
    Public marathons: no auth required.
    Private marathons: requires auth + participation check.
    """
    marathon = await get_marathon_or_404(db, marathon_id)

    # For private marathons, require authentication and participation
    if not marathon.is_public:
        if not credentials:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Authentication required for private marathon leaderboard",
                headers={"WWW-Authenticate": "Bearer"},
            )

        payload = decode_access_token(credentials.credentials)
        if not payload:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid or expired token",
                headers={"WWW-Authenticate": "Bearer"},
            )

        user_id = int(payload.get("sub"))
        participant = await get_participant(db, user_id, marathon_id)
        if not participant:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="You are not a participant of this marathon",
            )

    result = await db.execute(
        select(Participant)
        .options(
            selectinload(Participant.user).selectinload(User.equipped_frame),
            selectinload(Participant.user).selectinload(User.equipped_title),
            selectinload(Participant.user).selectinload(User.equipped_name_color),
            selectinload(Participant.user).selectinload(User.equipped_background),
        )
        .where(Participant.marathon_id == marathon_id)
        .order_by(Participant.total_points.desc())
    )
    participants = result.scalars().all()

    leaderboard = []
    for rank, p in enumerate(participants, 1):
        # Count completed and dropped assignments
        completed = await db.scalar(
            select(func.count()).select_from(Assignment).where(
                Assignment.participant_id == p.id,
                Assignment.status == AssignmentStatus.COMPLETED.value,
            )
        )
        dropped = await db.scalar(
            select(func.count()).select_from(Assignment).where(
                Assignment.participant_id == p.id,
                Assignment.status == AssignmentStatus.DROPPED.value,
            )
        )

        leaderboard.append(LeaderboardEntry(
            rank=rank,
            user=UserPublic.model_validate(p.user),
            total_points=p.total_points,
            current_streak=p.current_streak,
            completed_count=completed,
            dropped_count=dropped,
        ))

    return leaderboard


@router.get("/{marathon_id}/cover")
async def get_marathon_cover(marathon_id: int, db: DbSession):
    """Get marathon cover image"""
    marathon = await get_marathon_or_404(db, marathon_id)

    if not marathon.cover_path:
        raise HTTPException(status_code=404, detail="Marathon has no cover")

    file_data = await storage_service.get_file(marathon.cover_path, "covers")
    if not file_data:
        raise HTTPException(status_code=404, detail="Cover not found in storage")

    content, content_type = file_data

    return Response(
        content=content,
        media_type=content_type,
        headers={
            "Cache-Control": "public, max-age=3600",
        }
    )


@router.post("/{marathon_id}/cover", response_model=MarathonResponse)
async def upload_marathon_cover(
    marathon_id: int,
    current_user: CurrentUser,
    db: DbSession,
    file: UploadFile = File(...),
):
    """Upload marathon cover image (organizers only, preparing status)"""
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    if marathon.status != MarathonStatus.PREPARING.value:
        raise HTTPException(status_code=400, detail="Cannot update cover of active or finished marathon")

    # Validate file
    if not file.content_type or not file.content_type.startswith("image/"):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="File must be an image",
        )

    contents = await file.read()
    if len(contents) > settings.MAX_UPLOAD_SIZE:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"File too large. Maximum size is {settings.MAX_UPLOAD_SIZE // 1024 // 1024} MB",
        )

    # Get file extension
    ext = file.filename.split(".")[-1].lower() if file.filename else "jpg"
    if ext not in settings.ALLOWED_IMAGE_EXTENSIONS:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid file type. Allowed: {settings.ALLOWED_IMAGE_EXTENSIONS}",
        )

    # Delete old cover if exists
    if marathon.cover_path:
        await storage_service.delete_file(marathon.cover_path)

    # Upload file
    filename = storage_service.generate_filename(marathon_id, file.filename)
    file_path = await storage_service.upload_file(
        content=contents,
        folder="covers",
        filename=filename,
        content_type=file.content_type or "image/jpeg",
    )

    # Update marathon with cover path and URL
    marathon.cover_path = file_path
    marathon.cover_url = f"/api/v1/marathons/{marathon_id}/cover"
    await db.commit()

    return await get_marathon(marathon_id, current_user, db)


@router.delete("/{marathon_id}/cover", response_model=MarathonResponse)
async def delete_marathon_cover(
    marathon_id: int,
    current_user: CurrentUser,
    db: DbSession,
):
    """Delete marathon cover image (organizers only, preparing status)"""
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    if marathon.status != MarathonStatus.PREPARING.value:
        raise HTTPException(status_code=400, detail="Cannot update cover of active or finished marathon")

    if not marathon.cover_path:
        raise HTTPException(status_code=400, detail="Marathon has no cover")

    # Delete file from storage
    await storage_service.delete_file(marathon.cover_path)

    marathon.cover_path = None
    marathon.cover_url = None
    await db.commit()

    return await get_marathon(marathon_id, current_user, db)


# ============ Marathon Disputes (for organizers) ============
from pydantic import BaseModel, Field
from datetime import datetime


class MarathonDisputeResponse(BaseModel):
    id: int
    assignment_id: int | None
    bonus_assignment_id: int | None
    challenge_title: str
    participant_nickname: str
    raised_by_nickname: str
    reason: str
    status: str
    votes_valid: int
    votes_invalid: int
    created_at: str
    expires_at: str

    class Config:
        from_attributes = True


class ResolveDisputeRequest(BaseModel):
    is_valid: bool = Field(..., description="True = proof is valid, False = proof is invalid")


@router.get("/{marathon_id}/disputes", response_model=list[MarathonDisputeResponse])
async def list_marathon_disputes(
    marathon_id: int,
    current_user: CurrentUser,
    db: DbSession,
    status_filter: str = "open",
):
    """List disputes in a marathon. Organizers only."""
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    from datetime import timedelta
    DISPUTE_WINDOW_HOURS = 24

    # Get all assignments in this marathon (through games)
    games_result = await db.execute(
        select(Game.id).where(Game.marathon_id == marathon_id)
    )
    game_ids = [g[0] for g in games_result.all()]

    if not game_ids:
        return []

    # Get disputes for assignments in these games
    # Using selectinload for eager loading - no explicit joins needed
    query = (
        select(Dispute)
        .options(
            selectinload(Dispute.raised_by),
            selectinload(Dispute.votes),
            selectinload(Dispute.assignment).selectinload(Assignment.participant).selectinload(Participant.user),
            selectinload(Dispute.assignment).selectinload(Assignment.challenge),
            selectinload(Dispute.assignment).selectinload(Assignment.game),
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.main_assignment).selectinload(Assignment.participant).selectinload(Participant.user),
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.main_assignment).selectinload(Assignment.game),
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.challenge),
        )
        .order_by(Dispute.created_at.desc())
    )

    if status_filter == "open":
        query = query.where(Dispute.status == DisputeStatus.OPEN.value)

    result = await db.execute(query)
    all_disputes = result.scalars().unique().all()

    # Filter disputes that belong to this marathon's games
    response = []
    for dispute in all_disputes:
        # Check if dispute belongs to this marathon
        if dispute.bonus_assignment_id:
            bonus = dispute.bonus_assignment
            if not bonus or not bonus.main_assignment:
                continue
            if bonus.main_assignment.game_id not in game_ids:
                continue
            participant = bonus.main_assignment.participant
            challenge_title = f"Бонус: {bonus.challenge.title}"
        else:
            assignment = dispute.assignment
            if not assignment:
                continue
            if assignment.is_playthrough:
                if assignment.game_id not in game_ids:
                    continue
                challenge_title = f"Прохождение: {assignment.game.title}"
            else:
                if not assignment.challenge or assignment.challenge.game_id not in game_ids:
                    continue
                challenge_title = assignment.challenge.title
            participant = assignment.participant

        # Count votes
        votes_valid = sum(1 for v in dispute.votes if v.vote is True)
        votes_invalid = sum(1 for v in dispute.votes if v.vote is False)

        # Calculate expiry
        expires_at = dispute.created_at + timedelta(hours=DISPUTE_WINDOW_HOURS)

        response.append(MarathonDisputeResponse(
            id=dispute.id,
            assignment_id=dispute.assignment_id,
            bonus_assignment_id=dispute.bonus_assignment_id,
            challenge_title=challenge_title,
            participant_nickname=participant.user.nickname,
            raised_by_nickname=dispute.raised_by.nickname,
            reason=dispute.reason,
            status=dispute.status,
            votes_valid=votes_valid,
            votes_invalid=votes_invalid,
            created_at=dispute.created_at.isoformat(),
            expires_at=expires_at.isoformat(),
        ))

    return response


@router.post("/{marathon_id}/disputes/{dispute_id}/resolve", response_model=MessageResponse)
async def resolve_marathon_dispute(
    marathon_id: int,
    dispute_id: int,
    data: ResolveDisputeRequest,
    current_user: CurrentUser,
    db: DbSession,
):
    """Manually resolve a dispute in a marathon. Organizers only."""
    await require_organizer(db, current_user, marathon_id)
    marathon = await get_marathon_or_404(db, marathon_id)

    # Get dispute
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.assignment).selectinload(Assignment.participant),
            selectinload(Dispute.assignment).selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Dispute.assignment).selectinload(Assignment.game),
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.main_assignment).selectinload(Assignment.participant),
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.main_assignment).selectinload(Assignment.game),
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.challenge),
        )
        .where(Dispute.id == dispute_id)
    )
    dispute = result.scalar_one_or_none()

    if not dispute:
        raise HTTPException(status_code=404, detail="Dispute not found")

    # Verify dispute belongs to this marathon
    if dispute.bonus_assignment_id:
        bonus = dispute.bonus_assignment
        if bonus.main_assignment.game.marathon_id != marathon_id:
            raise HTTPException(status_code=403, detail="Dispute does not belong to this marathon")
    else:
        assignment = dispute.assignment
        if assignment.is_playthrough:
            if assignment.game.marathon_id != marathon_id:
                raise HTTPException(status_code=403, detail="Dispute does not belong to this marathon")
        else:
            if assignment.challenge.game.marathon_id != marathon_id:
                raise HTTPException(status_code=403, detail="Dispute does not belong to this marathon")

    if dispute.status != DisputeStatus.OPEN.value:
        raise HTTPException(status_code=400, detail="Dispute is already resolved")

    # Determine result
    if data.is_valid:
        result_status = DisputeStatus.RESOLVED_VALID.value
    else:
        result_status = DisputeStatus.RESOLVED_INVALID.value

        # Handle invalid proof
        if dispute.bonus_assignment_id:
            # Reset bonus assignment
            bonus = dispute.bonus_assignment
            main_assignment = bonus.main_assignment
            participant = main_assignment.participant

            # Only subtract points if main playthrough was already completed
            # (bonus points are added only when main playthrough is completed)
            if main_assignment.status == AssignmentStatus.COMPLETED.value:
                points_to_subtract = bonus.points_earned
                participant.total_points = max(0, participant.total_points - points_to_subtract)
                # Also reduce the points_earned on the main assignment
                main_assignment.points_earned = max(0, main_assignment.points_earned - points_to_subtract)

            bonus.status = BonusAssignmentStatus.PENDING.value
            bonus.proof_path = None
            bonus.proof_url = None
            bonus.proof_comment = None
            bonus.points_earned = 0
            bonus.completed_at = None
        else:
            # Reset main assignment
            assignment = dispute.assignment
            participant = assignment.participant

            # Subtract points
            points_to_subtract = assignment.points_earned
            participant.total_points = max(0, participant.total_points - points_to_subtract)

            # Reset streak - the completion was invalid
            participant.current_streak = 0

            # Reset assignment
            assignment.status = AssignmentStatus.RETURNED.value
            assignment.points_earned = 0

            # For playthrough: reset all bonus assignments
            if assignment.is_playthrough:
                bonus_result = await db.execute(
                    select(BonusAssignment).where(BonusAssignment.main_assignment_id == assignment.id)
                )
                for ba in bonus_result.scalars().all():
                    ba.status = BonusAssignmentStatus.PENDING.value
                    ba.proof_path = None
                    ba.proof_url = None
                    ba.proof_comment = None
                    ba.points_earned = 0
                    ba.completed_at = None

    # Update dispute
    dispute.status = result_status
    dispute.resolved_at = datetime.utcnow()

    await db.commit()

    # Send notification
    if dispute.bonus_assignment_id:
        participant_user_id = dispute.bonus_assignment.main_assignment.participant.user_id
        challenge_title = f"Бонус: {dispute.bonus_assignment.challenge.title}"
    elif dispute.assignment.is_playthrough:
        participant_user_id = dispute.assignment.participant.user_id
        challenge_title = f"Прохождение: {dispute.assignment.game.title}"
    else:
        participant_user_id = dispute.assignment.participant.user_id
        challenge_title = dispute.assignment.challenge.title

    await telegram_notifier.notify_dispute_resolved(
        db,
        user_id=participant_user_id,
        marathon_title=marathon.title,
        challenge_title=challenge_title,
        is_valid=data.is_valid
    )

    return MessageResponse(
        message=f"Dispute resolved as {'valid' if data.is_valid else 'invalid'}"
    )


# ============= Moderation Endpoints =============

@router.post("/{marathon_id}/participants/{user_id}/skip-assignment", response_model=MessageResponse)
async def organizer_skip_assignment(
    marathon_id: int,
    user_id: int,
    data: OrganizerSkipRequest,
    current_user: CurrentUser,
    db: DbSession,
):
    """
    Organizer skips a participant's current assignment.

    - No penalty for participant
    - Streak is preserved
    - Optionally exile the game from participant's pool
    """
    await require_organizer(db, current_user, marathon_id)

    # Get marathon
    result = await db.execute(
        select(Marathon).where(Marathon.id == marathon_id)
    )
    marathon = result.scalar_one_or_none()
    if not marathon:
        raise HTTPException(status_code=404, detail="Marathon not found")

    # Get target participant
    result = await db.execute(
        select(Participant)
        .options(selectinload(Participant.user))
        .where(
            Participant.marathon_id == marathon_id,
            Participant.user_id == user_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=404, detail="Participant not found")

    # Get active assignment (exclude event assignments)
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Assignment.game),
        )
        .where(
            Assignment.participant_id == participant.id,
            Assignment.status == AssignmentStatus.ACTIVE.value,
            Assignment.is_event_assignment == False,
        )
    )
    assignment = result.scalar_one_or_none()
    if not assignment:
        raise HTTPException(status_code=400, detail="Participant has no active assignment")

    # Get game info
    if assignment.is_playthrough:
        game = assignment.game
        game_id = game.id
        game_title = game.title
    else:
        game = assignment.challenge.game
        game_id = game.id
        game_title = game.title

    # Skip the assignment (no penalty)
    from datetime import datetime
    assignment.status = AssignmentStatus.DROPPED.value
    assignment.completed_at = datetime.utcnow()
    # Note: We do NOT reset streak or increment drop_count

    # Exile the game if requested
    if data.exile:
        # Check if already exiled
        existing = await db.execute(
            select(ExiledGame).where(
                ExiledGame.participant_id == participant.id,
                ExiledGame.game_id == game_id,
                ExiledGame.is_active == True,
            )
        )
        if not existing.scalar_one_or_none():
            exiled = ExiledGame(
                participant_id=participant.id,
                game_id=game_id,
                assignment_id=assignment.id,
                exiled_by="organizer",
                reason=data.reason,
            )
            db.add(exiled)

    # Log activity
    activity = Activity(
        marathon_id=marathon_id,
        user_id=current_user.id,
        type=ActivityType.MODERATION.value,
        data={
            "action": "skip_assignment",
            "target_user_id": user_id,
            "target_nickname": participant.user.nickname,
            "assignment_id": assignment.id,
            "game_id": game_id,
            "game_title": game_title,
            "exile": data.exile,
            "reason": data.reason,
        }
    )
    db.add(activity)

    await db.commit()

    # Send notification
    await telegram_notifier.notify_assignment_skipped_by_moderator(
        db,
        user=participant.user,
        marathon_title=marathon.title,
        game_title=game_title,
        exiled=data.exile,
        reason=data.reason,
        moderator_nickname=current_user.nickname,
    )

    exile_msg = " and exiled from pool" if data.exile else ""
    return MessageResponse(message=f"Assignment skipped{exile_msg}")


@router.get("/{marathon_id}/participants/{user_id}/exiled-games", response_model=list[ExiledGameResponse])
async def get_participant_exiled_games(
    marathon_id: int,
    user_id: int,
    current_user: CurrentUser,
    db: DbSession,
):
    """Get list of exiled games for a participant (organizers only)"""
    await require_organizer(db, current_user, marathon_id)

    # Get participant
    result = await db.execute(
        select(Participant).where(
            Participant.marathon_id == marathon_id,
            Participant.user_id == user_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=404, detail="Participant not found")

    # Get exiled games
    result = await db.execute(
        select(ExiledGame)
        .options(selectinload(ExiledGame.game))
        .where(
            ExiledGame.participant_id == participant.id,
            ExiledGame.is_active == True,
        )
        .order_by(ExiledGame.exiled_at.desc())
    )
    exiled_games = result.scalars().all()

    return [
        ExiledGameResponse(
            id=eg.id,
            game_id=eg.game_id,
            game_title=eg.game.title,
            exiled_at=eg.exiled_at,
            exiled_by=eg.exiled_by,
            reason=eg.reason,
        )
        for eg in exiled_games
    ]


@router.post("/{marathon_id}/participants/{user_id}/exiled-games/{game_id}/restore", response_model=MessageResponse)
async def restore_exiled_game(
    marathon_id: int,
    user_id: int,
    game_id: int,
    current_user: CurrentUser,
    db: DbSession,
):
    """Restore an exiled game back to participant's pool (organizers only)"""
    await require_organizer(db, current_user, marathon_id)

    # Get participant
    result = await db.execute(
        select(Participant).where(
            Participant.marathon_id == marathon_id,
            Participant.user_id == user_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=404, detail="Participant not found")

    # Get exiled game
    result = await db.execute(
        select(ExiledGame)
        .options(selectinload(ExiledGame.game))
        .where(
            ExiledGame.participant_id == participant.id,
            ExiledGame.game_id == game_id,
            ExiledGame.is_active == True,
        )
    )
    exiled_game = result.scalar_one_or_none()
    if not exiled_game:
        raise HTTPException(status_code=404, detail="Exiled game not found")

    # Restore (soft-delete)
    from datetime import datetime
    exiled_game.is_active = False
    exiled_game.unexiled_at = datetime.utcnow()
    exiled_game.unexiled_by = "organizer"

    await db.commit()

    return MessageResponse(message=f"Game '{exiled_game.game.title}' restored to pool")
