from datetime import timedelta
import secrets
import string
from fastapi import APIRouter, HTTPException, status, Depends
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.security import decode_access_token

# 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,
)
from app.schemas import (
    MarathonCreate,
    MarathonUpdate,
    MarathonResponse,
    MarathonListItem,
    MarathonPublicInfo,
    JoinMarathon,
    ParticipantInfo,
    ParticipantWithUser,
    LeaderboardEntry,
    MessageResponse,
    UserPublic,
    SetParticipantRole,
)
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,
        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))
        .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,
            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,
        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,
        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 games have at least one challenge
    games_without_challenges = []
    for game in approved_games:
        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):
    # 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

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