from __future__ import annotations

import json
import uuid
from datetime import datetime
from typing import Annotated, Any, Optional

from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from app.config import settings
from app.db import get_session
from app.models.entities import Poll, PollStatus, Room
from app.routers import ws as ws_router
from app.services import participants as participants_svc
from app.services.time_utils import as_utc_naive, utc_now
from app.services import voting as voting_svc

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


class RoomCreate(BaseModel):
    name: str = ""
    id: Optional[str] = None


class PollCreate(BaseModel):
    title: str
    options: list[str] = Field(min_length=2)
    trigger_names: list[str] = Field(default_factory=list)
    trigger_keys: list[str] = Field(default_factory=list)
    pause_action: dict[str, Any] = Field(default_factory=dict)
    winner_actions: dict[str, dict[str, Any]] = Field(default_factory=dict)
    duration_seconds: int = 0


class PollOpenBody(BaseModel):
    deadline_at: Optional[datetime] = None
    open_source: str = ""
    open_reason: str = ""
    trigger_event: Optional[dict[str, Any]] = None


class PollOverrideBody(BaseModel):
    winner_option: str
    action: Optional[dict[str, Any]] = None
    close_poll: bool = True


class PollResendBody(BaseModel):
    action: Optional[dict[str, Any]] = None


class VoteBody(BaseModel):
    voter_id: Optional[str] = None
    choice: str


async def require_host_key(x_host_key: Annotated[Optional[str], Header(alias="X-Host-Key")] = None):
    if not settings.host_api_key:
        return
    if x_host_key != settings.host_api_key:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="invalid host key")


def allow_player_trigger_open(body: PollOpenBody) -> bool:
    source = (body.open_source or "").strip().lower()
    reason = (body.open_reason or "").strip().lower()
    trigger = body.trigger_event if isinstance(body.trigger_event, dict) else None
    event_type = str((trigger or {}).get("event_type") or "").strip()
    return source == "player" and reason.startswith("hihaho_event:") and bool(event_type)


def ensure_can_open_poll(request: Request, body: PollOpenBody) -> str:
    x_host_key = request.headers.get("X-Host-Key")
    if not settings.host_api_key:
        return "no_host_key_configured"
    if x_host_key == settings.host_api_key:
        return "host_key"
    if allow_player_trigger_open(body):
        return "player_trigger"
    raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="invalid host key")


SessionDep = Annotated[AsyncSession, Depends(get_session)]


@router.post("/rooms", dependencies=[Depends(require_host_key)])
async def create_room(body: RoomCreate, session: SessionDep):
    rid = body.id or str(uuid.uuid4())
    room = await voting_svc.ensure_room(session, rid, body.name)
    await session.commit()
    await session.refresh(room)
    return {"id": room.id, "name": room.name}


@router.post("/rooms/{room_id}/polls", dependencies=[Depends(require_host_key)])
async def create_poll(room_id: str, body: PollCreate, session: SessionDep):
    await voting_svc.ensure_room(session, room_id)
    pid = str(uuid.uuid4())
    poll = Poll(
        id=pid,
        room_id=room_id,
        title=body.title,
        options_json=json.dumps(body.options),
        status=PollStatus.draft,
    )
    voting_svc.set_poll_config(
        poll,
        {
            "trigger_names": body.trigger_names,
            "trigger_keys": body.trigger_keys,
            "pause_action": body.pause_action,
            "winner_actions": body.winner_actions,
            "duration_seconds": body.duration_seconds,
        },
    )
    session.add(poll)
    await session.commit()
    await session.refresh(poll)
    await ws_router.broadcast_room_snapshot(room_id)
    return poll_out(poll)


def poll_out(poll: Poll) -> dict:
    return {
        "id": poll.id,
        "room_id": poll.room_id,
        "title": poll.title,
        "options": voting_svc.parse_options(poll),
        "status": poll.status.value,
        "winner_option": poll.winner_option,
        "winner_source": poll.winner_source,
        "deadline_at": poll.deadline_at.isoformat() if poll.deadline_at else None,
        "config": voting_svc.parse_poll_config(poll),
        "runtime": voting_svc.parse_poll_runtime(poll),
    }


@router.post("/polls/{poll_id}/open")
async def open_poll(poll_id: str, body: PollOpenBody, request: Request, session: SessionDep):
    auth_mode = ensure_can_open_poll(request, body)
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    if poll.status == PollStatus.open:
        return poll_out(poll)
    if auth_mode == "player_trigger" and poll.status != PollStatus.draft:
        raise HTTPException(409, "player trigger can only open a draft poll")
    deadline_at = as_utc_naive(body.deadline_at)
    if deadline_at and deadline_at <= utc_now():
        raise HTTPException(400, "deadline_at must be in the future")
    runtime = voting_svc.parse_poll_runtime(poll)
    runtime["opened_at"] = utc_now().isoformat() + "Z"
    runtime["open_source"] = body.open_source or "host"
    runtime["open_reason"] = body.open_reason or "manual_open"
    runtime["open_auth_mode"] = auth_mode
    if body.trigger_event is not None:
        runtime["last_open_event"] = body.trigger_event
    pause_command = voting_svc.build_open_pause_command(poll, runtime["open_source"])
    if pause_command:
        runtime["controller_command"] = pause_command
    voting_svc.set_poll_runtime(poll, runtime)
    poll.status = PollStatus.open
    poll.deadline_at = deadline_at
    poll.winner_option = None
    poll.winner_source = ""
    session.add(poll)
    await session.commit()
    await session.refresh(poll)
    await ws_router.broadcast_room_snapshot(poll.room_id)
    return poll_out(poll)


@router.post("/polls/{poll_id}/close", dependencies=[Depends(require_host_key)])
async def close_poll(poll_id: str, session: SessionDep):
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    await voting_svc.close_poll_and_set_winner(session, poll)
    await session.commit()
    await session.refresh(poll)
    await ws_router.broadcast_room_snapshot(poll.room_id)
    return poll_out(poll)


@router.post("/polls/{poll_id}/override", dependencies=[Depends(require_host_key)])
async def override_poll_winner(poll_id: str, body: PollOverrideBody, session: SessionDep):
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    winner = body.winner_option.strip()
    if winner and winner not in voting_svc.parse_options(poll):
        raise HTTPException(400, "winner_option must be one of poll options")

    if body.close_poll:
        poll.status = PollStatus.closed
    poll.winner_option = winner or None
    poll.winner_source = "host_override"
    runtime = voting_svc.parse_poll_runtime(poll)
    runtime["override_at"] = utc_now().isoformat() + "Z"
    runtime["override_source"] = "host"
    runtime["close_reason"] = "host_override"
    command = body.action or voting_svc.build_winner_command(poll, poll.winner_option, "host_override")
    if isinstance(command, dict):
        if "seq" not in command:
            command = {
                "seq": voting_svc.next_command_seq(poll),
                "winner_option": poll.winner_option,
                "source": "host_override",
                "action": command.get("action", command),
                "issued_at": utc_now().isoformat() + "Z",
            }
        runtime["controller_command"] = command
    voting_svc.set_poll_runtime(poll, runtime)
    session.add(poll)
    await session.commit()
    await session.refresh(poll)
    await ws_router.broadcast_room_snapshot(poll.room_id)
    return poll_out(poll)


@router.post("/polls/{poll_id}/control/resend", dependencies=[Depends(require_host_key)])
async def resend_control_command(poll_id: str, body: PollResendBody, session: SessionDep):
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    runtime = voting_svc.parse_poll_runtime(poll)
    current_command = runtime.get("controller_command") or {}
    action = body.action or current_command.get("action")
    if not isinstance(action, dict) or not action:
        raise HTTPException(400, "no action available to resend")
    runtime["controller_command"] = {
        "seq": voting_svc.next_command_seq(poll),
        "winner_option": poll.winner_option,
        "source": "host_resend",
        "action": action,
        "issued_at": utc_now().isoformat() + "Z",
    }
    runtime["last_resend_at"] = utc_now().isoformat() + "Z"
    voting_svc.set_poll_runtime(poll, runtime)
    session.add(poll)
    await session.commit()
    await session.refresh(poll)
    await ws_router.broadcast_room_snapshot(poll.room_id)
    return {"ok": True, "poll": poll_out(poll)}


@router.post("/polls/{poll_id}/votes")
async def post_vote(poll_id: str, body: VoteBody, request: Request, session: SessionDep):
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    voter_id = (body.voter_id or "").strip()
    sid = request.cookies.get("session_id")
    sid_rec = await participants_svc.get_session_record(session, sid)
    if sid_rec and sid_rec.room_id == poll.room_id:
        if voter_id and voter_id != sid_rec.participant_id:
            raise HTTPException(403, "voter_id mismatch with session")
        voter_id = sid_rec.participant_id
    if not voter_id:
        raise HTTPException(400, "voter_id is required")
    try:
        v = await voting_svc.cast_vote(session, poll, voter_id, body.choice)
    except ValueError as e:
        code = str(e)
        if code == "invalid_choice":
            raise HTTPException(400, "invalid choice")
        raise HTTPException(409, "poll not open or deadline passed")
    await session.commit()
    await ws_router.broadcast_room_snapshot(poll.room_id)
    return {"ok": True, "poll_id": poll_id, "choice": v.choice}


@router.get("/polls/{poll_id}")
async def get_poll(poll_id: str, session: SessionDep):
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    return poll_out(poll)


@router.get("/polls/{poll_id}/stats")
async def poll_stats(poll_id: str, session: SessionDep):
    poll = await session.get(Poll, poll_id)
    if not poll:
        raise HTTPException(404, "poll not found")
    tally = await voting_svc.compute_tally(session, poll_id)
    opts = voting_svc.parse_options(poll)
    for o in opts:
        tally.setdefault(o, 0)
    total = sum(tally.values())
    winner, tie = voting_svc.pick_winner(tally, opts)
    return {
        "poll_id": poll_id,
        "status": poll.status.value,
        "counts": tally,
        "total": total,
        "leader": winner,
        "tie": tie,
        "winner_option": poll.winner_option if poll.status == PollStatus.closed else None,
    }


@router.get("/polls/current")
async def current_poll(session: SessionDep, room_id: str = Query(..., description="room id")):
    result = await session.exec(
        select(Poll).where(Poll.room_id == room_id, Poll.status == PollStatus.open)
    )
    poll = result.first()
    if not poll:
        raise HTTPException(404, "no open poll for room")
    return poll_out(poll)


@router.get("/rooms/{room_id}/polls")
async def list_polls(room_id: str, session: SessionDep):
    result = await session.exec(select(Poll).where(Poll.room_id == room_id))
    polls = result.all()
    return [poll_out(p) for p in polls]
