import asyncio
import uuid
from dataclasses import dataclass, field
from datetime import datetime

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import settings
from app.database import async_session
from app.models import GameServer, Match, MatchPlayer, Mode, ServerStatus


@dataclass
class Ticket:
    ticket_id: uuid.UUID
    player_id: uuid.UUID
    callsign: str
    mode: Mode
    mmr: int
    queued_at: datetime = field(default_factory=datetime.utcnow)
    status: str = "queued"  # queued | matched | cancelled
    server_ip: str | None = None
    server_port: int | None = None
    team: int | None = None


class QueueManager:
    """In-memory matchmaking queue. No Redis: a single API process holds all
    ticket state, which is fine at this scale and simpler to run — see
    memory/spacewar_networking_plan for why Redis was deferred rather than
    included from the start.
    """

    def __init__(self) -> None:
        self._tickets: dict[uuid.UUID, Ticket] = {}
        self._lock = asyncio.Lock()

    async def join(self, player_id: uuid.UUID, callsign: str, mode: Mode, mmr: int) -> Ticket:
        ticket = Ticket(ticket_id=uuid.uuid4(), player_id=player_id, callsign=callsign, mode=mode, mmr=mmr)
        async with self._lock:
            self._tickets[ticket.ticket_id] = ticket
        return ticket

    async def leave(self, ticket_id: uuid.UUID) -> bool:
        async with self._lock:
            ticket = self._tickets.get(ticket_id)
            if ticket is None or ticket.status != "queued":
                return False
            ticket.status = "cancelled"
            return True

    def get(self, ticket_id: uuid.UUID) -> Ticket | None:
        return self._tickets.get(ticket_id)

    def _waiting(self, mode: Mode) -> list[Ticket]:
        tickets = [t for t in self._tickets.values() if t.mode == mode and t.status == "queued"]
        tickets.sort(key=lambda t: t.queued_at)
        return tickets

    async def _try_form_match(self, mode: Mode, required: int, cap: int, db: AsyncSession) -> None:
        async with self._lock:
            waiting = self._waiting(mode)
            if len(waiting) < required:
                return
            group = waiting[:cap]

            server = (
                await db.execute(
                    select(GameServer).where(GameServer.mode == mode, GameServer.status == ServerStatus.available)
                )
            ).scalars().first()
            if server is None:
                return  # no server free this cycle; leave everyone queued and retry next tick

            match = Match(mode=mode, server_id=server.id)
            db.add(match)
            await db.flush()

            for i, ticket in enumerate(group):
                team = i % 2
                db.add(MatchPlayer(match_id=match.id, player_id=ticket.player_id, team=team))
                ticket.status = "matched"
                ticket.server_ip = server.ip
                ticket.server_port = server.port
                ticket.team = team

            await db.commit()

    async def run_matching_loop(self) -> None:
        while True:
            await asyncio.sleep(2)
            async with async_session() as db:
                # Bot fill (spacewar/bots/bot_manager.gd) pads both teams up
                # to GameConfig.bot_min_team_size, so a single queued player
                # is enough to form a match. Still group in anyone else who
                # queues in the same tick, up to the real casual_team_size*2
                # target, rather than capping at 1v1.
                await self._try_form_match(Mode.casual, required=1, cap=settings.casual_team_size * 2, db=db)


queue_manager = QueueManager()
