"""Роутер конференций: создание, «Мои конференции», календарь, резолв, вход, правки (ADR-001)."""

import uuid
from datetime import UTC, datetime, timedelta
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy.ext.asyncio import AsyncSession

from api.deps import get_current_user
from core.db import get_session
from core.rate_limit import (
    RATE_LIMIT_MISS_MAX_REQUESTS,
    RATE_LIMIT_SOFT_MAX_REQUESTS,
    client_ip,
    enforce_rate_limit,
)
from models.user import User
from schemas.conferences import (
    ConferenceCreateIn,
    ConferenceOut,
    ConferenceUpdateIn,
    GuestJoinIn,
    JoinIn,
    JoinOut,
    MuteParticipantIn,
    MuteParticipantOut,
    OccurrenceOut,
    ResolveOut,
)
from services.conference_access import (
    ConferenceEndedError,
    InvalidPasswordError,
    PasswordRequiredError,
)
from services.conferences import (
    ConferenceActiveError,
    ConferenceNotFoundError,
    ConferenceService,
    InvalidConferenceStateError,
    InviteeUserNotFoundError,
    NotConferenceOwnerError,
)
from services.room_control import ParticipantNotInRoomError

router = APIRouter(prefix="/api/v1/conferences", tags=["conferences"])

# Максимальная ширина диапазона `from`/`to` для GET /calendar — защита от
# случайного запроса на годы вперёд (календарь UI показывает недели/месяцы).
MAX_CALENDAR_RANGE = timedelta(days=62)


@router.post("", status_code=status.HTTP_201_CREATED, response_model=ConferenceOut)
async def create_conference(
    data: ConferenceCreateIn,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> ConferenceOut:
    """Создать конференцию: без `scheduled_at` — мгновенная (создатель входит сразу же)."""
    service = ConferenceService(session)
    try:
        conference, join = await service.create(owner=user, data=data)
    except InviteeUserNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="invitee_user_not_found"
        ) from exc
    return await service.to_detail_out(conference, viewer=user, join=join)


@router.get("/my", response_model=list[ConferenceOut])
async def list_my_conferences(
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> list[ConferenceOut]:
    """Закреплённые конференции + предстоящие разовые владельца."""
    service = ConferenceService(session)
    return await service.list_my(owner=user)


@router.get("/calendar", response_model=list[OccurrenceOut])
async def get_calendar(
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
    from_: Annotated[datetime, Query(alias="from")],
    to: Annotated[datetime, Query()],
) -> list[OccurrenceOut]:
    """Развёртка вхождений закреплённых (с повторением) и разовых плановых конференций владельца."""
    t_from = _require_utc(from_)
    t_to = _require_utc(to)
    if t_to <= t_from or t_to - t_from > MAX_CALENDAR_RANGE:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="invalid_range"
        )
    service = ConferenceService(session)
    return await service.list_calendar(owner=user, t_from=t_from, t_to=t_to)


@router.get("/resolve", response_model=ResolveOut)
async def resolve_conference(
    request: Request,
    session: Annotated[AsyncSession, Depends(get_session)],
    q: Annotated[str, Query(min_length=1)],
) -> ResolveOut:
    """Найти конференцию по номеру или ссылке — без auth; rate limit; единообразный 404.

    Для завершённой конференции (`status=ended`) отдаём минимальный ответ —
    только `id`/`title`/`status`, без `is_closed`/`requires_password` (ADR-001,
    п.4, уточнение резолва): вход в неё невозможен в любом случае (410 у
    join/guest-join), а признак закрытости неактуален для мёртвой конференции.
    """
    ip = client_ip(request)
    # Мягкий потолок против флуда: успешные резолвы легитимны и массовы —
    # вся конференция открывает ссылку в одну минуту.
    await enforce_rate_limit(f"resolve:{ip}", max_requests=RATE_LIMIT_SOFT_MAX_REQUESTS)
    service = ConferenceService(session)
    conference = await service.resolve(q)
    if conference is None:
        # Жёсткий счётчик — только на промахи: перебор номера конференции
        # выглядит именно так (см. core/rate_limit.py и ADR-001, п.4).
        await enforce_rate_limit(
            f"resolve_miss:{ip}", max_requests=RATE_LIMIT_MISS_MAX_REQUESTS
        )
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not_found")
    if conference.status == "ended":
        return ResolveOut(id=conference.id, title=conference.title, status=conference.status)
    return ResolveOut(
        id=conference.id,
        title=conference.title,
        status=conference.status,
        is_closed=conference.is_closed,
        requires_password=conference.is_closed,
    )


@router.post("/{conference_id}/join", response_model=JoinOut)
async def join_conference(
    conference_id: uuid.UUID,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
    data: JoinIn = JoinIn(),
) -> JoinOut:
    """Войти в конференцию зарегистрированным пользователем."""
    service = ConferenceService(session)
    try:
        return await service.join_as_user(conference_id, user=user, password=data.password)
    except ConferenceNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
        ) from exc
    except ConferenceEndedError as exc:
        raise HTTPException(status_code=status.HTTP_410_GONE, detail="conference_ended") from exc
    except PasswordRequiredError as exc:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="password_required"
        ) from exc
    except InvalidPasswordError as exc:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="invalid_password"
        ) from exc


@router.post("/{conference_id}/guest-join", response_model=JoinOut)
async def guest_join_conference(
    conference_id: uuid.UUID,
    request: Request,
    data: GuestJoinIn,
    session: Annotated[AsyncSession, Depends(get_session)],
) -> JoinOut:
    """Войти гостем: представиться (имя обязательно, email факультативен) — без auth, rate limit."""
    ip = client_ip(request)
    # Мягкий потолок: успешный гостевой вход — обычное дело для всей
    # конференции сразу, ограничивать его числом «10 в минуту» нельзя.
    await enforce_rate_limit(f"guest_join:{ip}", max_requests=RATE_LIMIT_SOFT_MAX_REQUESTS)
    service = ConferenceService(session)
    try:
        return await service.join_as_guest(conference_id, data=data)
    except ConferenceNotFoundError as exc:
        await _count_guest_join_miss(ip)
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
        ) from exc
    except ConferenceEndedError as exc:
        raise HTTPException(status_code=status.HTTP_410_GONE, detail="conference_ended") from exc
    except PasswordRequiredError as exc:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="password_required"
        ) from exc
    except InvalidPasswordError as exc:
        # Подбор пароля закрытой конференции — тот же класс атаки, что и
        # перебор номера, поэтому считается жёстким счётчиком.
        await _count_guest_join_miss(ip)
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="invalid_password"
        ) from exc


@router.post("/{conference_id}/mute-participant", response_model=MuteParticipantOut)
async def mute_participant(
    conference_id: uuid.UUID,
    data: MuteParticipantIn,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> MuteParticipantOut:
    """Принудительно выключить микрофон/камеру участника (задача B2) — владелец/администратор.

    Права проверяются ЗАНОВО по владельцу конференции в БД
    (`ConferenceService.mute_participant`), а не по метаданным LiveKit-токена
    вызывающего — те лишь подсказка для UI и потенциально подделываемы клиентом.
    """
    service = ConferenceService(session)
    try:
        muted = await service.mute_participant(
            conference_id, actor=user, target_identity=data.identity, source=data.source
        )
    except ConferenceNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
        ) from exc
    except NotConferenceOwnerError as exc:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not_owner") from exc
    except ParticipantNotInRoomError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="participant_not_in_room"
        ) from exc
    return MuteParticipantOut(muted=muted)


@router.get("/{conference_id}", response_model=ConferenceOut)
async def get_conference(
    conference_id: uuid.UUID,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> ConferenceOut:
    """Детальная карточка конференции (с полным составом участников); владелец или администратор."""
    service = ConferenceService(session)
    try:
        conference = await service.get_detail(conference_id, actor=user)
    except ConferenceNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
        ) from exc
    except NotConferenceOwnerError as exc:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not_owner") from exc
    return await service.to_detail_out(conference, viewer=user)


@router.patch("/{conference_id}", response_model=ConferenceOut)
async def update_conference(
    conference_id: uuid.UUID,
    data: ConferenceUpdateIn,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> ConferenceOut:
    """Изменить конференцию: разрешено владельцу или администратору."""
    service = ConferenceService(session)
    try:
        conference = await service.update(conference_id, actor=user, data=data)
    except ConferenceNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
        ) from exc
    except NotConferenceOwnerError as exc:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not_owner") from exc
    except InvalidConferenceStateError as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
        ) from exc
    except InviteeUserNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="invitee_user_not_found"
        ) from exc
    return await service.to_detail_out(conference, viewer=user)


@router.delete("/{conference_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_conference(
    conference_id: uuid.UUID,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> None:
    """Удалить конференцию: запрещено для активной (409), разрешено владельцу/администратору."""
    service = ConferenceService(session)
    try:
        await service.delete(conference_id, actor=user)
    except ConferenceNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
        ) from exc
    except NotConferenceOwnerError as exc:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not_owner") from exc
    except ConferenceActiveError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT, detail="conference_active"
        ) from exc


def _require_utc(value: datetime) -> datetime:
    """Требовать явную таймзону и привести значение к UTC (в БД и API — только UTC)."""
    if value.tzinfo is None:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="datetime_must_be_timezone_aware",
        )
    return value.astimezone(UTC)


async def _count_guest_join_miss(ip: str) -> None:
    """Учесть неудачную попытку гостевого входа в жёстком счётчике.

    Вынесено отдельно, потому что вызывается из двух веток обработки ошибок
    (несуществующая конференция и неверный пароль) и обязано бросать 429
    ровно так же, как обычный `enforce_rate_limit`.
    """
    await enforce_rate_limit(f"guest_join_miss:{ip}", max_requests=RATE_LIMIT_MISS_MAX_REQUESTS)
