from collections.abc import Sequence
from datetime import UTC, datetime

import structlog
from sqlalchemy import and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.database.models import ContestAttempt, ContestRound, ContestTemplate, User


logger = structlog.get_logger(__name__)


# Templates
async def get_template_by_id(db: AsyncSession, template_id: int) -> ContestTemplate | None:
    result = await db.execute(select(ContestTemplate).where(ContestTemplate.id == template_id))
    return result.scalar_one_or_none()


async def get_template_by_slug(db: AsyncSession, slug: str) -> ContestTemplate | None:
    result = await db.execute(select(ContestTemplate).where(ContestTemplate.slug == slug))
    return result.scalar_one_or_none()


async def list_templates(db: AsyncSession, enabled_only: bool = True) -> list[ContestTemplate]:
    query = select(ContestTemplate).order_by(ContestTemplate.id)
    if enabled_only:
        query = query.where(ContestTemplate.is_enabled.is_(True))
    result = await db.execute(query)
    return list(result.scalars().all())


async def upsert_template(
    db: AsyncSession,
    *,
    slug: str,
    name: str,
    description: str = '',
    prize_type: str = 'days',
    prize_value: str = '1',
    max_winners: int = 1,
    attempts_per_user: int = 1,
    times_per_day: int = 1,
    schedule_times: str | None = None,
    cooldown_hours: int = 24,
    payload: dict | None = None,
    is_enabled: bool | None = None,
) -> ContestTemplate:
    template = await get_template_by_slug(db, slug)
    if not template:
        template = ContestTemplate(slug=slug)
        db.add(template)

    template.name = name
    template.description = description
    template.prize_type = prize_type
    template.prize_value = prize_value
    template.max_winners = max_winners
    template.attempts_per_user = attempts_per_user
    template.times_per_day = times_per_day
    template.schedule_times = schedule_times
    template.cooldown_hours = cooldown_hours
    template.payload = payload or {}
    if is_enabled is not None:
        template.is_enabled = is_enabled
    await db.commit()
    await db.refresh(template)
    return template


async def update_template_fields(
    db: AsyncSession,
    template: ContestTemplate,
    **fields: object,
) -> ContestTemplate:
    for key, value in fields.items():
        if hasattr(template, key):
            setattr(template, key, value)
    await db.commit()
    await db.refresh(template)
    return template


# Rounds
async def create_round(
    db: AsyncSession,
    *,
    template: ContestTemplate,
    starts_at: datetime,
    ends_at: datetime,
    payload: dict,
) -> ContestRound:
    round_obj = ContestRound(
        template_id=template.id,
        starts_at=starts_at,
        ends_at=ends_at,
        status='active',
        payload=payload,
        max_winners=template.max_winners,
        attempts_per_user=template.attempts_per_user,
    )
    db.add(round_obj)
    await db.commit()
    await db.refresh(round_obj)
    return round_obj


async def get_active_rounds(db: AsyncSession) -> list[ContestRound]:
    now = datetime.now(UTC)
    result = await db.execute(
        select(ContestRound)
        .options(selectinload(ContestRound.template))
        .where(
            and_(
                ContestRound.status == 'active',
                ContestRound.starts_at <= now,
                ContestRound.ends_at >= now,
            )
        )
        .order_by(ContestRound.starts_at)
    )
    return list(result.scalars().all())


async def get_active_round_by_template(db: AsyncSession, template_id: int) -> ContestRound | None:
    now = datetime.now(UTC)
    result = await db.execute(
        select(ContestRound)
        .options(selectinload(ContestRound.template))
        .where(
            and_(
                ContestRound.template_id == template_id,
                ContestRound.status == 'active',
                ContestRound.starts_at <= now,
                ContestRound.ends_at >= now,
            )
        )
        .order_by(desc(ContestRound.starts_at))
    )
    return result.scalars().first()


async def finish_round(db: AsyncSession, round_obj: ContestRound) -> ContestRound:
    round_obj.status = 'finished'
    await db.commit()
    await db.refresh(round_obj)
    return round_obj


async def increment_winner_count(db: AsyncSession, round_obj: ContestRound) -> ContestRound:
    round_obj.winners_count += 1
    await db.commit()
    await db.refresh(round_obj)
    return round_obj


# Attempts
async def get_attempt(db: AsyncSession, round_id: int, user_id: int) -> ContestAttempt | None:
    result = await db.execute(
        select(ContestAttempt).where(
            and_(
                ContestAttempt.round_id == round_id,
                ContestAttempt.user_id == user_id,
            )
        )
    )
    return result.scalar_one_or_none()


async def create_attempt(
    db: AsyncSession,
    *,
    round_id: int,
    user_id: int,
    answer: str | None,
    is_winner: bool,
) -> ContestAttempt:
    attempt = ContestAttempt(
        round_id=round_id,
        user_id=user_id,
        answer=answer,
        is_winner=is_winner,
    )
    db.add(attempt)
    await db.commit()
    await db.refresh(attempt)
    return attempt


async def update_attempt(
    db: AsyncSession,
    attempt: ContestAttempt,
    *,
    answer: str | None = None,
    is_winner: bool = False,
) -> ContestAttempt:
    """Update existing attempt with answer and winner status."""
    if answer is not None:
        attempt.answer = answer
    attempt.is_winner = is_winner
    await db.commit()
    await db.refresh(attempt)
    return attempt


async def clear_attempts(db: AsyncSession, round_id: int) -> int:
    result = await db.execute(delete(ContestAttempt).where(ContestAttempt.round_id == round_id))
    deleted_count = result.rowcount
    await db.commit()
    return deleted_count


async def list_winners(db: AsyncSession, round_id: int) -> Sequence[tuple[User, ContestAttempt]]:
    result = await db.execute(
        select(User, ContestAttempt)
        .join(ContestAttempt, ContestAttempt.user_id == User.id)
        .where(
            and_(
                ContestAttempt.round_id == round_id,
                ContestAttempt.is_winner.is_(True),
            )
        )
    )
    return result.all()
