"""
Assignment details and dispute system endpoints.
"""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, HTTPException, Request
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,
)
from app.schemas import (
    AssignmentDetailResponse, DisputeCreate, DisputeResponse,
    DisputeCommentCreate, DisputeCommentResponse, DisputeVoteCreate,
    MessageResponse, ChallengeResponse, GameShort, ReturnedAssignmentResponse,
)
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"""
    # Get assignment with all relationships
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            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")

    # Check user is participant of the marathon
    marathon_id = 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")

    # Build response
    challenge = assignment.challenge
    game = challenge.game
    owner_user = assignment.participant.user

    # Determine if user can dispute
    can_dispute = False
    if (
        assignment.status == AssignmentStatus.COMPLETED.value
        and assignment.completed_at
        and assignment.participant.user_id != current_user.id
        and assignment.dispute is None
    ):
        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")

    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"),
            ),
            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_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),
        )
        .where(Assignment.id == assignment_id)
    )
    assignment = result.scalar_one_or_none()

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

    # Check user is participant of the marathon
    marathon_id = 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 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.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"""
    # Get assignment
    result = await db.execute(
        select(Assignment)
        .options(
            selectinload(Assignment.challenge).selectinload(Challenge.game),
            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")

    # Check user is participant of the marathon
    marathon_id = 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")

    # 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")

    if assignment.dispute:
        raise HTTPException(status_code=400, detail="A 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:
        await telegram_notifier.notify_dispute_raised(
            db,
            user_id=assignment.participant.user_id,
            marathon_title=marathon.title,
            challenge_title=assignment.challenge.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
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.assignment).selectinload(Assignment.challenge).selectinload(Challenge.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
    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
    result = await db.execute(
        select(Dispute)
        .options(
            selectinload(Dispute.assignment).selectinload(Assignment.challenge).selectinload(Challenge.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
    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.dispute),
        )
        .where(
            Assignment.participant_id == participant.id,
            Assignment.status == AssignmentStatus.RETURNED.value,
        )
        .order_by(Assignment.completed_at.asc())  # Oldest first
    )
    assignments = result.scalars().all()

    return [
        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"),
                ),
                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 "Оспорено",
        )
        for a in assignments
    ]
