"""
Assignment details and dispute system endpoints.
"""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
from fastapi.responses import Response, StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import selectinload

from app.api.deps import DbSession, CurrentUser
from app.models import (
    Assignment, AssignmentStatus, Participant, Challenge, User, Marathon,
    Dispute, DisputeStatus, DisputeComment, DisputeVote, BonusAssignment, BonusAssignmentStatus,
)
from app.schemas import (
    AssignmentDetailResponse, DisputeCreate, DisputeResponse,
    DisputeCommentCreate, DisputeCommentResponse, DisputeVoteCreate,
    MessageResponse, ChallengeResponse, GameShort, ReturnedAssignmentResponse,
    BonusAssignmentResponse, CompleteBonusAssignment, BonusCompleteResult,
)
from app.schemas.user import UserPublic
from app.services.storage import storage_service
from app.services.telegram_notifier import telegram_notifier

router = APIRouter(tags=["assignments"])

# Dispute window: 24 hours after completion
DISPUTE_WINDOW_HOURS = 24


def user_to_public(user: User) -> UserPublic:
    """Convert User model to UserPublic schema"""
    return UserPublic(
        id=user.id,
        login=user.login,
        nickname=user.nickname,
        avatar_url=None,
        role=user.role,
        created_at=user.created_at,
    )


def build_dispute_response(dispute: Dispute, current_user_id: int) -> DisputeResponse:
    """Build DisputeResponse from Dispute model"""
    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)

    my_vote = None
    for v in dispute.votes:
        if v.user_id == current_user_id:
            my_vote = v.vote
            break

    # Ensure expires_at has UTC timezone info for correct frontend parsing
    created_at_utc = dispute.created_at.replace(tzinfo=timezone.utc) if dispute.created_at.tzinfo is None else dispute.created_at
    expires_at = created_at_utc + timedelta(hours=DISPUTE_WINDOW_HOURS)

    return DisputeResponse(
        id=dispute.id,
        raised_by=user_to_public(dispute.raised_by),
        reason=dispute.reason,
        status=dispute.status,
        comments=[
            DisputeCommentResponse(
                id=c.id,
                user=user_to_public(c.user),
                text=c.text,
                created_at=c.created_at,
            )
            for c in sorted(dispute.comments, key=lambda x: x.created_at)
        ],
        votes=[
            {
                "user": user_to_public(v.user),
                "vote": v.vote,
                "created_at": v.created_at,
            }
            for v in dispute.votes
        ],
        votes_valid=votes_valid,
        votes_invalid=votes_invalid,
        my_vote=my_vote,
        expires_at=expires_at,
        created_at=dispute.created_at,
        resolved_at=dispute.resolved_at,
    )


@router.get("/assignments/{assignment_id}", response_model=AssignmentDetailResponse)
async def get_assignment_detail(
    assignment_id: int,
    current_user: CurrentUser,
    db: DbSession,
):
    """Get detailed information about an assignment including proofs and dispute"""
    from app.models import Game

    # Get assignment with all relationships
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Assignment.game),  # For playthrough
            selectinload(Assignment.proof_files),  # Load multiple proof files
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.challenge),  # For playthrough
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.proof_files),  # Load bonus proof files
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.dispute).selectinload(Dispute.raised_by),  # Bonus disputes
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.dispute).selectinload(Dispute.comments).selectinload(DisputeComment.user),
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.dispute).selectinload(Dispute.votes).selectinload(DisputeVote.user),
            selectinload(Assignment.participant).selectinload(Participant.user),
            selectinload(Assignment.dispute).selectinload(Dispute.raised_by),
            selectinload(Assignment.dispute).selectinload(Dispute.comments).selectinload(DisputeComment.user),
            selectinload(Assignment.dispute).selectinload(Dispute.votes).selectinload(DisputeVote.user),
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")

    # Get marathon_id based on assignment type
    if assignment.is_playthrough:
        marathon_id = assignment.game.marathon_id
    else:
        marathon_id = assignment.challenge.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    owner_user = assignment.participant.user

    # Determine if user can dispute (including playthrough)
    # Allow disputing if no active dispute exists (resolved disputes don't block new ones)
    has_active_dispute = (
        assignment.dispute is not None and
        assignment.dispute.status in [DisputeStatus.OPEN.value, DisputeStatus.PENDING_ADMIN.value]
    )
    can_dispute = False
    if (
        assignment.status == AssignmentStatus.COMPLETED.value
        and assignment.completed_at
        and assignment.participant.user_id != current_user.id
        and not has_active_dispute
    ):
        time_since_completion = datetime.utcnow() - assignment.completed_at
        can_dispute = time_since_completion < timedelta(hours=DISPUTE_WINDOW_HOURS)

    # Build proof URLs
    proof_image_url = storage_service.get_url(assignment.proof_path, "proofs")

    # Handle playthrough assignments
    if assignment.is_playthrough:
        game = assignment.game
        bonus_challenges = []
        for ba in assignment.bonus_assignments:
            # Determine if user can dispute this bonus
            # Allow disputing if no active dispute exists
            bonus_has_active_dispute = (
                ba.dispute is not None and
                ba.dispute.status in [DisputeStatus.OPEN.value, DisputeStatus.PENDING_ADMIN.value]
            )
            bonus_can_dispute = False
            if (
                ba.status == BonusAssignmentStatus.COMPLETED.value
                and ba.completed_at
                and assignment.participant.user_id != current_user.id
                and not bonus_has_active_dispute
            ):
                time_since_bonus_completion = datetime.utcnow() - ba.completed_at
                bonus_can_dispute = time_since_bonus_completion < timedelta(hours=DISPUTE_WINDOW_HOURS)

            # Build bonus proof files list
            from app.schemas.assignment import ProofFileResponse
            bonus_proof_files = [
                ProofFileResponse(
                    id=pf.id,
                    file_type=pf.file_type,
                    order_index=pf.order_index,
                    created_at=pf.created_at,
                )
                for pf in ba.proof_files
            ]

            bonus_challenges.append({
                "id": ba.id,
                "challenge": {
                    "id": ba.challenge.id,
                    "title": ba.challenge.title,
                    "description": ba.challenge.description,
                    "points": ba.challenge.points,
                    "difficulty": ba.challenge.difficulty,
                    "proof_hint": ba.challenge.proof_hint,
                },
                "status": ba.status,
                "proof_url": ba.proof_url,
                "proof_image_url": storage_service.get_url(ba.proof_path, "bonus_proofs") if ba.proof_path else None,
                "proof_files": bonus_proof_files,
                "proof_comment": ba.proof_comment,
                "points_earned": ba.points_earned,
                "completed_at": ba.completed_at.isoformat() if ba.completed_at else None,
                "can_dispute": bonus_can_dispute,
                "dispute": build_dispute_response(ba.dispute, current_user.id) if ba.dispute else None,
            })

        # Build proof files list
        from app.schemas.assignment import ProofFileResponse
        proof_files_list = [
            ProofFileResponse(
                id=pf.id,
                file_type=pf.file_type,
                order_index=pf.order_index,
                created_at=pf.created_at,
            )
            for pf in assignment.proof_files
        ]

        return AssignmentDetailResponse(
            id=assignment.id,
            challenge=None,
            game=GameShort(
                id=game.id,
                title=game.title,
                cover_url=storage_service.get_url(game.cover_path, "covers"),
                download_url=game.download_url,
                game_type=game.game_type,
            ),
            is_playthrough=True,
            playthrough_info={
                "description": game.playthrough_description,
                "points": game.playthrough_points,
                "proof_type": game.playthrough_proof_type,
                "proof_hint": game.playthrough_proof_hint,
            },
            participant=user_to_public(owner_user),
            status=assignment.status,
            proof_url=assignment.proof_url,
            proof_image_url=proof_image_url,
            proof_files=proof_files_list,
            proof_comment=assignment.proof_comment,
            points_earned=assignment.points_earned,
            streak_at_completion=assignment.streak_at_completion,
            started_at=assignment.started_at,
            completed_at=assignment.completed_at,
            can_dispute=can_dispute,
            dispute=build_dispute_response(assignment.dispute, current_user.id) if assignment.dispute else None,
            bonus_challenges=bonus_challenges,
        )

    # Regular challenge assignment
    challenge = assignment.challenge
    game = challenge.game

    # Build proof files list
    from app.schemas.assignment import ProofFileResponse
    proof_files_list = [
        ProofFileResponse(
            id=pf.id,
            file_type=pf.file_type,
            order_index=pf.order_index,
            created_at=pf.created_at,
        )
        for pf in assignment.proof_files
    ]

    return AssignmentDetailResponse(
        id=assignment.id,
        challenge=ChallengeResponse(
            id=challenge.id,
            title=challenge.title,
            description=challenge.description,
            type=challenge.type,
            difficulty=challenge.difficulty,
            points=challenge.points,
            estimated_time=challenge.estimated_time,
            proof_type=challenge.proof_type,
            proof_hint=challenge.proof_hint,
            game=GameShort(
                id=game.id,
                title=game.title,
                cover_url=storage_service.get_url(game.cover_path, "covers"),
                download_url=game.download_url,
            ),
            is_generated=challenge.is_generated,
            created_at=challenge.created_at,
        ),
        participant=user_to_public(owner_user),
        status=assignment.status,
        proof_url=assignment.proof_url,
        proof_image_url=proof_image_url,
        proof_files=proof_files_list,
        proof_comment=assignment.proof_comment,
        points_earned=assignment.points_earned,
        streak_at_completion=assignment.streak_at_completion,
        started_at=assignment.started_at,
        completed_at=assignment.completed_at,
        can_dispute=can_dispute,
        dispute=build_dispute_response(assignment.dispute, current_user.id) if assignment.dispute else None,
    )


@router.get("/assignments/{assignment_id}/proof-media")
async def get_assignment_proof_media(
    assignment_id: int,
    request: Request,
    current_user: CurrentUser,
    db: DbSession,
):
    """Stream the proof media (image or video) for an assignment with Range support"""
    # Get assignment
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Assignment.game),  # For playthrough
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")

    # Get marathon_id based on assignment type
    if assignment.is_playthrough:
        marathon_id = assignment.game.marathon_id
    else:
        marathon_id = assignment.challenge.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Check if proof exists
    if not assignment.proof_path:
        raise HTTPException(status_code=404, detail="No proof media for this assignment")

    # Get file from storage
    file_data = await storage_service.get_file(assignment.proof_path, "proofs")
    if not file_data:
        raise HTTPException(status_code=404, detail="Proof media not found in storage")

    content, content_type = file_data
    file_size = len(content)

    # Check if it's a video and handle Range requests
    is_video = content_type.startswith("video/")

    if is_video:
        range_header = request.headers.get("range")

        if range_header:
            # Parse range header
            range_match = range_header.replace("bytes=", "").split("-")
            start = int(range_match[0]) if range_match[0] else 0
            end = int(range_match[1]) if range_match[1] else file_size - 1

            # Ensure valid range
            if start >= file_size:
                raise HTTPException(status_code=416, detail="Range not satisfiable")

            end = min(end, file_size - 1)
            chunk = content[start:end + 1]

            return Response(
                content=chunk,
                status_code=206,
                media_type=content_type,
                headers={
                    "Content-Range": f"bytes {start}-{end}/{file_size}",
                    "Accept-Ranges": "bytes",
                    "Content-Length": str(len(chunk)),
                    "Cache-Control": "public, max-age=31536000",
                }
            )

        # No range header - return full video with Accept-Ranges
        return Response(
            content=content,
            media_type=content_type,
            headers={
                "Accept-Ranges": "bytes",
                "Content-Length": str(file_size),
                "Cache-Control": "public, max-age=31536000",
            }
        )

    # For images, just return the content
    return Response(
        content=content,
        media_type=content_type,
        headers={
            "Cache-Control": "public, max-age=31536000",
        }
    )


# Keep old endpoint for backwards compatibility
@router.get("/assignments/{assignment_id}/proof-image")
async def get_assignment_proof_image(
    assignment_id: int,
    request: Request,
    current_user: CurrentUser,
    db: DbSession,
):
    """Deprecated: Use proof-media instead. Redirects to proof-media."""
    return await get_assignment_proof_media(assignment_id, request, current_user, db)


@router.get("/assignments/{assignment_id}/bonus/{bonus_id}/proof-media")
async def get_bonus_proof_media(
    assignment_id: int,
    bonus_id: int,
    request: Request,
    current_user: CurrentUser,
    db: DbSession,
):
    """Stream the proof media (image or video) for a bonus assignment"""
    # Get assignment with bonus assignments
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.game),
            selectinload(Assignment.bonus_assignments),
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")

    if not assignment.is_playthrough:
        raise HTTPException(status_code=400, detail="This is not a playthrough assignment")

    # Get marathon_id
    marathon_id = assignment.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Find the bonus assignment
    bonus_assignment = None
    for ba in assignment.bonus_assignments:
        if ba.id == bonus_id:
            bonus_assignment = ba
            break

    if not bonus_assignment:
        raise HTTPException(status_code=404, detail="Bonus assignment not found")

    # Check if proof exists
    if not bonus_assignment.proof_path:
        raise HTTPException(status_code=404, detail="No proof media for this bonus assignment")

    # Get file from storage
    file_data = await storage_service.get_file(bonus_assignment.proof_path, "bonus_proofs")
    if not file_data:
        raise HTTPException(status_code=404, detail="Proof media not found in storage")

    content, content_type = file_data
    file_size = len(content)

    # Check if it's a video and handle Range requests
    is_video = content_type.startswith("video/")

    if is_video:
        range_header = request.headers.get("range")

        if range_header:
            range_match = range_header.replace("bytes=", "").split("-")
            start = int(range_match[0]) if range_match[0] else 0
            end = int(range_match[1]) if range_match[1] else file_size - 1

            if start >= file_size:
                raise HTTPException(status_code=416, detail="Range not satisfiable")

            end = min(end, file_size - 1)
            chunk = content[start:end + 1]

            return Response(
                content=chunk,
                status_code=206,
                media_type=content_type,
                headers={
                    "Content-Range": f"bytes {start}-{end}/{file_size}",
                    "Accept-Ranges": "bytes",
                    "Content-Length": str(len(chunk)),
                    "Cache-Control": "public, max-age=31536000",
                }
            )

        return Response(
            content=content,
            media_type=content_type,
            headers={
                "Accept-Ranges": "bytes",
                "Content-Length": str(file_size),
                "Cache-Control": "public, max-age=31536000",
            }
        )

    # For images, just return the content
    return Response(
        content=content,
        media_type=content_type,
        headers={
            "Cache-Control": "public, max-age=31536000",
        }
    )


@router.get("/assignments/{assignment_id}/proof-files/{proof_file_id}/media")
async def get_assignment_proof_file_media(
    assignment_id: int,
    proof_file_id: int,
    request: Request,
    current_user: CurrentUser,
    db: DbSession,
):
    """Stream a specific proof file (image or video) for an assignment"""
    from app.models import AssignmentProof, Game

    # Get proof file
    result = await db.execute(
        select(AssignmentProof)
        .options(
            selectinload(AssignmentProof.assignment).selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(AssignmentProof.assignment).selectinload(Assignment.game),  # For playthrough
        )
        .where(
            AssignmentProof.id == proof_file_id,
            AssignmentProof.assignment_id == assignment_id,
        )
    )
    proof_file = result.scalar_one_or_none()

    if not proof_file:
        raise HTTPException(status_code=404, detail="Proof file not found")

    assignment = proof_file.assignment

    # Get marathon_id based on assignment type
    if assignment.is_playthrough:
        marathon_id = assignment.game.marathon_id
    else:
        marathon_id = assignment.challenge.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Get file from storage
    file_data = await storage_service.get_file(proof_file.file_path, "proofs")
    if not file_data:
        raise HTTPException(status_code=404, detail="Proof file not found in storage")

    content, content_type = file_data
    file_size = len(content)

    # Check if it's a video and handle Range requests
    is_video = content_type.startswith("video/")

    if is_video:
        range_header = request.headers.get("range")

        if range_header:
            # Parse range header
            range_match = range_header.replace("bytes=", "").split("-")
            start = int(range_match[0]) if range_match[0] else 0
            end = int(range_match[1]) if len(range_match) > 1 and range_match[1] else file_size - 1

            chunk = content[start:end + 1]

            return Response(
                content=chunk,
                status_code=206,
                media_type=content_type,
                headers={
                    "Content-Range": f"bytes {start}-{end}/{file_size}",
                    "Accept-Ranges": "bytes",
                    "Content-Length": str(len(chunk)),
                    "Cache-Control": "public, max-age=31536000",
                }
            )

        return Response(
            content=content,
            media_type=content_type,
            headers={
                "Accept-Ranges": "bytes",
                "Content-Length": str(file_size),
                "Cache-Control": "public, max-age=31536000",
            }
        )

    # For images, just return the content
    return Response(
        content=content,
        media_type=content_type,
        headers={
            "Cache-Control": "public, max-age=31536000",
        }
    )


@router.get("/assignments/{assignment_id}/bonus/{bonus_id}/proof-files/{proof_file_id}/media")
async def get_bonus_proof_file_media(
    assignment_id: int,
    bonus_id: int,
    proof_file_id: int,
    request: Request,
    current_user: CurrentUser,
    db: DbSession,
):
    """Stream a specific proof file (image or video) for a bonus assignment"""
    from app.models import BonusAssignmentProof, Game

    # Get proof file
    result = await db.execute(
        select(BonusAssignmentProof)
        .options(
            selectinload(BonusAssignmentProof.bonus_assignment)
            .selectinload(BonusAssignment.main_assignment)
            .selectinload(Assignment.game),  # For playthrough
        )
        .where(
            BonusAssignmentProof.id == proof_file_id,
            BonusAssignmentProof.bonus_assignment_id == bonus_id,
        )
    )
    proof_file = result.scalar_one_or_none()

    if not proof_file:
        raise HTTPException(status_code=404, detail="Proof file not found")

    bonus = proof_file.bonus_assignment
    assignment = bonus.main_assignment

    # Check assignment matches
    if assignment.id != assignment_id:
        raise HTTPException(status_code=404, detail="Proof file not found for this assignment")

    # Get marathon_id (bonus assignments are always for playthrough)
    marathon_id = assignment.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Get file from storage
    file_data = await storage_service.get_file(proof_file.file_path, "bonus_proofs")
    if not file_data:
        raise HTTPException(status_code=404, detail="Proof file not found in storage")

    content, content_type = file_data
    file_size = len(content)

    # Check if it's a video and handle Range requests
    is_video = content_type.startswith("video/")

    if is_video:
        range_header = request.headers.get("range")

        if range_header:
            # Parse range header
            range_match = range_header.replace("bytes=", "").split("-")
            start = int(range_match[0]) if range_match[0] else 0
            end = int(range_match[1]) if len(range_match) > 1 and range_match[1] else file_size - 1

            chunk = content[start:end + 1]

            return Response(
                content=chunk,
                status_code=206,
                media_type=content_type,
                headers={
                    "Content-Range": f"bytes {start}-{end}/{file_size}",
                    "Accept-Ranges": "bytes",
                    "Content-Length": str(len(chunk)),
                    "Cache-Control": "public, max-age=31536000",
                }
            )

        return Response(
            content=content,
            media_type=content_type,
            headers={
                "Accept-Ranges": "bytes",
                "Content-Length": str(file_size),
                "Cache-Control": "public, max-age=31536000",
            }
        )

    # For images, just return the content
    return Response(
        content=content,
        media_type=content_type,
        headers={
            "Cache-Control": "public, max-age=31536000",
        }
    )


@router.post("/bonus-assignments/{bonus_id}/dispute", response_model=DisputeResponse)
async def create_bonus_dispute(
    bonus_id: int,
    data: DisputeCreate,
    current_user: CurrentUser,
    db: DbSession,
):
    """Create a dispute against a bonus assignment's proof"""
    from app.models import Game

    # Get bonus assignment with main assignment
    result = await db.execute(
        select(BonusAssignment)
        .options(
            selectinload(BonusAssignment.main_assignment).selectinload(Assignment.game),
            selectinload(BonusAssignment.main_assignment).selectinload(Assignment.participant),
            selectinload(BonusAssignment.challenge),
            selectinload(BonusAssignment.dispute),
        )
        .where(BonusAssignment.id == bonus_id)
    )
    bonus_assignment = result.scalar_one_or_none()

    if not bonus_assignment:
        raise HTTPException(status_code=404, detail="Bonus assignment not found")

    main_assignment = bonus_assignment.main_assignment
    marathon_id = main_assignment.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Validate
    if bonus_assignment.status != BonusAssignmentStatus.COMPLETED.value:
        raise HTTPException(status_code=400, detail="Can only dispute completed bonus assignments")

    if main_assignment.participant.user_id == current_user.id:
        raise HTTPException(status_code=400, detail="Cannot dispute your own bonus assignment")

    # Check for active dispute (open or pending admin decision)
    if bonus_assignment.dispute and bonus_assignment.dispute.status in [DisputeStatus.OPEN.value, DisputeStatus.PENDING_ADMIN.value]:
        raise HTTPException(status_code=400, detail="An active dispute already exists for this bonus assignment")

    if not bonus_assignment.completed_at:
        raise HTTPException(status_code=400, detail="Bonus assignment has no completion date")

    time_since_completion = datetime.utcnow() - bonus_assignment.completed_at
    if time_since_completion >= timedelta(hours=DISPUTE_WINDOW_HOURS):
        raise HTTPException(status_code=400, detail="Dispute window has expired (24 hours)")

    # Create dispute for bonus assignment
    dispute = Dispute(
        bonus_assignment_id=bonus_id,
        raised_by_id=current_user.id,
        reason=data.reason,
        status=DisputeStatus.OPEN.value,
    )
    db.add(dispute)
    await db.commit()
    await db.refresh(dispute)

    # Send notification to assignment owner
    result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
    marathon = result.scalar_one_or_none()
    if marathon:
        title = f"Бонус: {bonus_assignment.challenge.title}"
        await telegram_notifier.notify_dispute_raised(
            db,
            user_id=main_assignment.participant.user_id,
            marathon_title=marathon.title,
            challenge_title=title,
            assignment_id=main_assignment.id
        )

    # Load relationships for response
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.raised_by),
            selectinload(Dispute.comments).selectinload(DisputeComment.user),
            selectinload(Dispute.votes).selectinload(DisputeVote.user),
        )
        .where(Dispute.id == dispute.id)
    )
    dispute = result.scalar_one()

    return build_dispute_response(dispute, current_user.id)


@router.post("/assignments/{assignment_id}/dispute", response_model=DisputeResponse)
async def create_dispute(
    assignment_id: int,
    data: DisputeCreate,
    current_user: CurrentUser,
    db: DbSession,
):
    """Create a dispute against an assignment's proof (including playthrough)"""
    # Get assignment
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Assignment.game),  # For playthrough
            selectinload(Assignment.participant),
            selectinload(Assignment.dispute),
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")

    # Get marathon_id based on assignment type
    if assignment.is_playthrough:
        marathon_id = assignment.game.marathon_id
    else:
        marathon_id = assignment.challenge.game.marathon_id

    # Check user is participant of the marathon
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Validate
    if assignment.status != AssignmentStatus.COMPLETED.value:
        raise HTTPException(status_code=400, detail="Can only dispute completed assignments")

    if assignment.participant.user_id == current_user.id:
        raise HTTPException(status_code=400, detail="Cannot dispute your own assignment")

    # Check for active dispute (open or pending admin decision)
    if assignment.dispute and assignment.dispute.status in [DisputeStatus.OPEN.value, DisputeStatus.PENDING_ADMIN.value]:
        raise HTTPException(status_code=400, detail="An active dispute already exists for this assignment")

    if not assignment.completed_at:
        raise HTTPException(status_code=400, detail="Assignment has no completion date")

    time_since_completion = datetime.utcnow() - assignment.completed_at
    if time_since_completion >= timedelta(hours=DISPUTE_WINDOW_HOURS):
        raise HTTPException(status_code=400, detail="Dispute window has expired (24 hours)")

    # Create dispute
    dispute = Dispute(
        assignment_id=assignment_id,
        raised_by_id=current_user.id,
        reason=data.reason,
        status=DisputeStatus.OPEN.value,
    )
    db.add(dispute)
    await db.commit()
    await db.refresh(dispute)

    # Send notification to assignment owner
    result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
    marathon = result.scalar_one_or_none()
    if marathon:
        # Get title based on assignment type
        if assignment.is_playthrough:
            title = f"Прохождение: {assignment.game.title}"
        else:
            title = assignment.challenge.title

        await telegram_notifier.notify_dispute_raised(
            db,
            user_id=assignment.participant.user_id,
            marathon_title=marathon.title,
            challenge_title=title,
            assignment_id=assignment_id
        )

    # Load relationships for response
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.raised_by),
            selectinload(Dispute.comments).selectinload(DisputeComment.user),
            selectinload(Dispute.votes).selectinload(DisputeVote.user),
        )
        .where(Dispute.id == dispute.id)
    )
    dispute = result.scalar_one()

    return build_dispute_response(dispute, current_user.id)


@router.post("/disputes/{dispute_id}/comments", response_model=DisputeCommentResponse)
async def add_dispute_comment(
    dispute_id: int,
    data: DisputeCommentCreate,
    current_user: CurrentUser,
    db: DbSession,
):
    """Add a comment to a dispute discussion"""
    # Get dispute with assignment or bonus assignment
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.assignment).selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Dispute.assignment).selectinload(Assignment.game),  # For playthrough
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.main_assignment).selectinload(Assignment.game),
        )
        .where(Dispute.id == dispute_id)
    )
    dispute = result.scalar_one_or_none()

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

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

    # Check user is participant of the marathon
    if dispute.bonus_assignment_id:
        marathon_id = dispute.bonus_assignment.main_assignment.game.marathon_id
    elif dispute.assignment.is_playthrough:
        marathon_id = dispute.assignment.game.marathon_id
    else:
        marathon_id = dispute.assignment.challenge.game.marathon_id
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Create comment
    comment = DisputeComment(
        dispute_id=dispute_id,
        user_id=current_user.id,
        text=data.text,
    )
    db.add(comment)
    await db.commit()
    await db.refresh(comment)

    # Get user for response
    result = await db.execute(select(User).where(User.id == current_user.id))
    user = result.scalar_one()

    return DisputeCommentResponse(
        id=comment.id,
        user=user_to_public(user),
        text=comment.text,
        created_at=comment.created_at,
    )


@router.post("/disputes/{dispute_id}/vote", response_model=MessageResponse)
async def vote_on_dispute(
    dispute_id: int,
    data: DisputeVoteCreate,
    current_user: CurrentUser,
    db: DbSession,
):
    """Vote on a dispute (True = valid/proof is OK, False = invalid/proof is not OK)"""
    # Get dispute with assignment or bonus assignment
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.assignment).selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Dispute.assignment).selectinload(Assignment.game),  # For playthrough
            selectinload(Dispute.bonus_assignment).selectinload(BonusAssignment.main_assignment).selectinload(Assignment.game),
        )
        .where(Dispute.id == dispute_id)
    )
    dispute = result.scalar_one_or_none()

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

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

    # Check user is participant of the marathon
    if dispute.bonus_assignment_id:
        marathon_id = dispute.bonus_assignment.main_assignment.game.marathon_id
    elif dispute.assignment and dispute.assignment.is_playthrough:
        marathon_id = dispute.assignment.game.marathon_id
    else:
        marathon_id = dispute.assignment.challenge.game.marathon_id
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Check if user already voted
    result = await db.execute(
        select(DisputeVote).where(
            DisputeVote.dispute_id == dispute_id,
            DisputeVote.user_id == current_user.id,
        )
    )
    existing_vote = result.scalar_one_or_none()

    if existing_vote:
        # Update existing vote
        existing_vote.vote = data.vote
        existing_vote.created_at = datetime.utcnow()
    else:
        # Create new vote
        vote = DisputeVote(
            dispute_id=dispute_id,
            user_id=current_user.id,
            vote=data.vote,
        )
        db.add(vote)

    await db.commit()

    vote_text = "валидным" if data.vote else "невалидным"
    return MessageResponse(message=f"Вы проголосовали: пруф {vote_text}")


@router.get("/marathons/{marathon_id}/returned-assignments", response_model=list[ReturnedAssignmentResponse])
async def get_returned_assignments(
    marathon_id: int,
    current_user: CurrentUser,
    db: DbSession,
):
    """Get current user's returned assignments that need to be redone"""
    # Check user is participant
    result = await db.execute(
        select(Participant).where(
            Participant.user_id == current_user.id,
            Participant.marathon_id == marathon_id,
        )
    )
    participant = result.scalar_one_or_none()
    if not participant:
        raise HTTPException(status_code=403, detail="You are not a participant of this marathon")

    # Get returned assignments
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            selectinload(Assignment.game),  # For playthrough assignments
            selectinload(Assignment.dispute),
        )
        .where(
            Assignment.participant_id == participant.id,
            Assignment.status == AssignmentStatus.RETURNED.value,
        )
        .order_by(Assignment.completed_at.asc())  # Oldest first
    )
    assignments = result.scalars().all()

    response = []
    for a in assignments:
        if a.is_playthrough:
            # Playthrough assignment
            response.append(ReturnedAssignmentResponse(
                id=a.id,
                is_playthrough=True,
                game_id=a.game_id,
                game_title=a.game.title if a.game else None,
                game_cover_url=storage_service.get_url(a.game.cover_path, "covers") if a.game else None,
                original_completed_at=a.completed_at,
                dispute_reason=a.dispute.reason if a.dispute else "Оспорено",
            ))
        else:
            # Challenge assignment
            response.append(ReturnedAssignmentResponse(
                id=a.id,
                challenge=ChallengeResponse(
                    id=a.challenge.id,
                    title=a.challenge.title,
                    description=a.challenge.description,
                    type=a.challenge.type,
                    difficulty=a.challenge.difficulty,
                    points=a.challenge.points,
                    estimated_time=a.challenge.estimated_time,
                    proof_type=a.challenge.proof_type,
                    proof_hint=a.challenge.proof_hint,
                    game=GameShort(
                        id=a.challenge.game.id,
                        title=a.challenge.game.title,
                        cover_url=storage_service.get_url(a.challenge.game.cover_path, "covers"),
                        download_url=a.challenge.game.download_url,
                    ),
                    is_generated=a.challenge.is_generated,
                    created_at=a.challenge.created_at,
                ),
                original_completed_at=a.completed_at,
                dispute_reason=a.dispute.reason if a.dispute else "Оспорено",
            ))

    return response


# ============ Bonus Assignments Endpoints ============

@router.get("/assignments/{assignment_id}/bonus", response_model=list[BonusAssignmentResponse])
async def get_bonus_assignments(
    assignment_id: int,
    current_user: CurrentUser,
    db: DbSession,
):
    """Get bonus assignments for a playthrough assignment"""
    # Get assignment with bonus challenges
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.game),
            selectinload(Assignment.participant),
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.challenge),
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")

    if not assignment.is_playthrough:
        raise HTTPException(status_code=400, detail="This is not a playthrough assignment")

    # Check user is the owner
    if assignment.participant.user_id != current_user.id:
        raise HTTPException(status_code=403, detail="You can only view your own bonus assignments")

    # Build response
    return [
        BonusAssignmentResponse(
            id=ba.id,
            challenge=ChallengeResponse(
                id=ba.challenge.id,
                title=ba.challenge.title,
                description=ba.challenge.description,
                type=ba.challenge.type,
                difficulty=ba.challenge.difficulty,
                points=ba.challenge.points,
                estimated_time=ba.challenge.estimated_time,
                proof_type=ba.challenge.proof_type,
                proof_hint=ba.challenge.proof_hint,
                game=GameShort(
                    id=assignment.game.id,
                    title=assignment.game.title,
                    cover_url=storage_service.get_url(assignment.game.cover_path, "covers") if hasattr(assignment.game, 'cover_path') else None,
                    download_url=assignment.game.download_url,
                    game_type=assignment.game.game_type,
                ),
                is_generated=ba.challenge.is_generated,
                created_at=ba.challenge.created_at,
            ),
            status=ba.status,
            proof_url=ba.proof_url,
            proof_comment=ba.proof_comment,
            points_earned=ba.points_earned,
            completed_at=ba.completed_at,
        )
        for ba in assignment.bonus_assignments
    ]


@router.post("/assignments/{assignment_id}/bonus/{bonus_id}/complete", response_model=BonusCompleteResult)
async def complete_bonus_assignment(
    assignment_id: int,
    bonus_id: int,
    current_user: CurrentUser,
    db: DbSession,
    proof_url: str | None = Form(None),
    comment: str | None = Form(None),
    proof_file: UploadFile | None = File(None),  # Legacy single file support
    proof_files: list[UploadFile] = File([]),  # Multiple files support
):
    """Complete a bonus challenge for a playthrough assignment"""
    from app.core.config import settings

    # Get main assignment
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.participant),
            selectinload(Assignment.bonus_assignments).selectinload(BonusAssignment.challenge),
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

    if not assignment:
        raise HTTPException(status_code=404, detail="Assignment not found")

    if not assignment.is_playthrough:
        raise HTTPException(status_code=400, detail="This is not a playthrough assignment")

    # Check user is the owner
    if assignment.participant.user_id != current_user.id:
        raise HTTPException(status_code=403, detail="You can only complete your own bonus assignments")

    # Check main assignment is active or completed (completed allows re-doing bonus after bonus dispute)
    if assignment.status not in [AssignmentStatus.ACTIVE.value, AssignmentStatus.COMPLETED.value]:
        raise HTTPException(
            status_code=400,
            detail="Bonus challenges can only be completed while the main assignment is active or completed"
        )

    # Find the bonus assignment
    bonus_assignment = None
    for ba in assignment.bonus_assignments:
        if ba.id == bonus_id:
            bonus_assignment = ba
            break

    if not bonus_assignment:
        raise HTTPException(status_code=404, detail="Bonus assignment not found")

    if bonus_assignment.status == BonusAssignmentStatus.COMPLETED.value:
        raise HTTPException(status_code=400, detail="This bonus challenge is already completed")

    # Combine legacy single file with new multiple files
    all_files = []
    if proof_file:
        all_files.append(proof_file)
    if proof_files:
        all_files.extend(proof_files)

    # Validate proof (need file(s), URL, or comment)
    if not all_files and not proof_url and not comment:
        raise HTTPException(
            status_code=400,
            detail="Необходимо прикрепить файл, ссылку или комментарий"
        )

    # Handle multiple file uploads
    if all_files:
        from app.models import BonusAssignmentProof

        for idx, file in enumerate(all_files):
            contents = await file.read()
            if len(contents) > settings.MAX_UPLOAD_SIZE:
                raise HTTPException(
                    status_code=400,
                    detail=f"File {file.filename} too large. Maximum size is {settings.MAX_UPLOAD_SIZE // 1024 // 1024} MB",
                )

            ext = file.filename.split(".")[-1].lower() if file.filename else "jpg"
            if ext not in settings.ALLOWED_EXTENSIONS:
                raise HTTPException(
                    status_code=400,
                    detail=f"Invalid file type for {file.filename}. Allowed: {settings.ALLOWED_EXTENSIONS}",
                )

            # Determine file type (image or video)
            file_type = "video" if ext in ["mp4", "webm", "mov", "avi"] else "image"

            # Upload file to storage
            filename = storage_service.generate_filename(f"bonus_{bonus_id}_{idx}", file.filename)
            file_path = await storage_service.upload_file(
                content=contents,
                folder="bonus_proofs",
                filename=filename,
                content_type=file.content_type or "application/octet-stream",
            )

            # Create BonusAssignmentProof record
            proof_record = BonusAssignmentProof(
                bonus_assignment_id=bonus_id,
                file_path=file_path,
                file_type=file_type,
                order_index=idx
            )
            db.add(proof_record)

            # Legacy: set proof_path on first file for backward compatibility
            if idx == 0:
                bonus_assignment.proof_path = file_path

    # Set proof URL if provided
    if proof_url:
        bonus_assignment.proof_url = proof_url

    # Complete the bonus assignment
    # NOTE: We store BASE points here. Event multiplier is applied when main assignment is completed
    # This ensures multiplier is applied to the SUM (base + all bonuses), not separately
    bonus_assignment.points_earned = bonus_assignment.challenge.points
    bonus_assignment.status = BonusAssignmentStatus.COMPLETED.value
    bonus_assignment.proof_comment = comment
    bonus_assignment.completed_at = datetime.utcnow()

    # If main assignment is already COMPLETED, add bonus points immediately
    # This handles the case where a bonus was disputed and user is re-completing it
    if assignment.status == AssignmentStatus.COMPLETED.value:
        from app.models import EventType
        from app.services.points import PointsService

        # Apply event multiplier if assignment had one
        points_to_add = bonus_assignment.points_earned
        if assignment.event_type:
            ps = PointsService()
            multiplier = ps.EVENT_MULTIPLIERS.get(assignment.event_type, 1.0)
            points_to_add = int(bonus_assignment.points_earned * multiplier)
            # Update bonus assignment to show multiplied points
            bonus_assignment.points_earned = points_to_add

        participant = assignment.participant
        participant.total_points += points_to_add
        assignment.points_earned += points_to_add

    # NOTE: If main is not completed yet, points will be added when main is completed
    # This prevents exploiting by dropping the main assignment after getting bonus points

    await db.commit()

    # Calculate total bonus points for this assignment
    total_bonus_points = sum(
        ba.points_earned for ba in assignment.bonus_assignments
        if ba.status == BonusAssignmentStatus.COMPLETED.value
    )

    return BonusCompleteResult(
        bonus_assignment_id=bonus_assignment.id,
        points_earned=bonus_assignment.points_earned,
        total_bonus_points=total_bonus_points,
    )
