"""Pydantic-схемы для конференций (`/api/v1/conferences`) и их join-потока (ADR-001)."""

import uuid
from datetime import UTC, datetime, timedelta

from pydantic import BaseModel, EmailStr, Field, field_serializer, field_validator, model_validator

from core.plugins.config import PublishQualityCap, StageMaxTiles, SummaryRecipientsMode
from schemas.room_events import ForcedMuteSource
from services.recurrence import RecurrenceRule

# Допуск в прошлое при плановом создании/правке — небольшой запас на задержку
# сети/рассинхронизацию часов клиента (перенесено из старых `schemas/bookings.py`).
PAST_TOLERANCE = timedelta(minutes=1)


def _to_iso_z(value: datetime) -> str:
    """Отформатировать aware-datetime как UTC ISO-строку с суффиксом `Z`."""
    return value.astimezone(UTC).isoformat().replace("+00:00", "Z")


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


class InviteeIn(BaseModel):
    """Один приглашённый участник в теле создания/правки конференции (ADR-003).

    Ровно одно из `user_id`/`email` — зарегистрированный пользователь ИЛИ
    внешний адрес; email нормализуется в lower-case (совпадает с хранением в
    `conference_invitees.email`).
    """

    user_id: uuid.UUID | None = None
    email: EmailStr | None = None

    @field_validator("email")
    @classmethod
    def _normalize_email(cls, value: str | None) -> str | None:
        return value.lower() if value is not None else None

    @model_validator(mode="after")
    def _validate(self) -> "InviteeIn":
        if (self.user_id is None) == (self.email is None):
            raise ValueError("invitee_requires_exactly_one_identity")
        return self


class InviteeOut(BaseModel):
    """Приглашённый в ответе API. Организатор — всегда первый элемент `participants`."""

    user_id: uuid.UUID | None = None
    email: str | None = None
    name: str | None = None
    avatar_url: str | None = None
    is_organizer: bool = False


class ConferenceCreateIn(BaseModel):
    """Тело запроса создания конференции.

    Без `scheduled_at` — мгновенная конференция (создатель входит сразу же,
    ответ содержит `join`); с `scheduled_at` — плановая (`status=scheduled`).
    """

    title: str | None = Field(default=None, max_length=255)
    scheduled_at: datetime | None = None
    duration_minutes: int | None = Field(default=None, gt=0)
    is_pinned: bool = False
    recurrence: RecurrenceRule | None = None
    is_closed: bool = False
    password: str | None = Field(default=None, min_length=4)
    # Переопределение рассылки саммари: `None` — дефолт
    # инстанса (`instance_settings['summary_recipients']`).
    summary_recipients: SummaryRecipientsMode | None = None
    # Состав приглашённых (ADR-003); `None` — без участников (кроме
    # организатора, который добавляется автоматически и неудаляемо).
    participants: list[InviteeIn] | None = None

    @field_validator("scheduled_at")
    @classmethod
    def _normalize_scheduled_at(cls, value: datetime | None) -> datetime | None:
        return _require_aware_utc(value) if value is not None else None

    @model_validator(mode="after")
    def _validate(self) -> "ConferenceCreateIn":
        if self.is_closed and not self.password:
            raise ValueError("closed_conference_requires_password")
        if self.recurrence is not None and not self.is_pinned:
            raise ValueError("recurrence_requires_pinned")
        # Запрет прошлого — только для обычных (не закреплённых) конференций.
        # Закреплённая конференция — не разовое событие с конкретной датой, а
        # постоянная комната-ссылка (см. комментарий у `is_pinned` в
        # `models/conference.py`); `scheduled_at` для неё — не более чем
        # подсказка в календаре, и прошедшая дата (например, «встречаемся тут
        # каждый день с такого-то числа», уже наступившего) осмысленна.
        if (
            not self.is_pinned
            and self.scheduled_at is not None
            and self.scheduled_at < datetime.now(UTC) - PAST_TOLERANCE
        ):
            raise ValueError("scheduled_at_in_the_past")
        return self


class ConferenceUpdateIn(BaseModel):
    """Тело запроса частичной правки конференции — все поля опциональны."""

    title: str | None = Field(default=None, max_length=255)
    scheduled_at: datetime | None = None
    duration_minutes: int | None = Field(default=None, gt=0)
    is_pinned: bool | None = None
    recurrence: RecurrenceRule | None = None
    is_closed: bool | None = None
    password: str | None = Field(default=None, min_length=4)
    # `None` не различает «не передано» и «явный сброс на дефолт инстанса» —
    # сервис читает `model_fields_set` (тот же паттерн, что у `recurrence`).
    summary_recipients: SummaryRecipientsMode | None = None
    # `None` — не менять состав; список — полная замена (diff считает backend,
    # ADR-003, п.3). Организатор неудаляем и в списке не нужен — молча
    # дедуплицируется, если всё же передан.
    participants: list[InviteeIn] | None = None

    @field_validator("scheduled_at")
    @classmethod
    def _normalize_scheduled_at(cls, value: datetime | None) -> datetime | None:
        return _require_aware_utc(value) if value is not None else None


class JoinOut(BaseModel):
    """Данные, необходимые клиенту для подключения к LiveKit-комнате конференции."""

    livekit_url: str
    token: str
    room_name: str
    conference_id: uuid.UUID
    # Тоггл инстанса `chat.enabled` на момент входа — клиент решает,
    # показывать ли UI чата, не дожидаясь ошибки WS-подключения.
    chat_enabled: bool
    # Тоггл инстанса `hand_queue.enabled` на момент входа — гасит модуль
    # «поднятие руки» целиком (кнопка + очередь), см. `HandQueueConfig`.
    hand_queue_enabled: bool
    # Рычаги нагрузки медиа (`instance_settings.media_limits`) — отдаются
    # прямо в join-ответе, а не только в админке: участнику нужно иметь их
    # на руках ДО публикации своего трека (см. `services/conference_access.py`).
    publish_quality_cap: PublishQualityCap
    stage_max_tiles: StageMaxTiles
    # Тоггл инстанса `virtual_background.enabled` — гасит замену фона целиком
    # (кнопка в тулбаре комнаты и применение процессора к треку). Едет здесь,
    # а не через `/admin/settings` (доступен только админу): значение нужно
    # участнику — в том числе гостю — до первого рендера комнаты.
    virtual_background_enabled: bool


class ConferenceOut(BaseModel):
    """Конференция в ответе API.

    `participants` заполняется только в детальных ответах (создание, правка,
    `GET /conferences/{id}`) — списочные эндпоинты (`/my`, `/calendar`) состав
    не раздувают и оставляют его пустым (ADR-003, п.5).
    """

    id: uuid.UUID
    number: str
    slug: str
    title: str | None
    status: str
    is_pinned: bool
    is_closed: bool
    scheduled_at: datetime | None
    duration_minutes: int | None
    recurrence: RecurrenceRule | None
    next_occurrence: datetime | None = None
    created_at: datetime
    join: JoinOut | None = None
    # `None` = используется дефолт инстанса (`instance_settings['summary_recipients']`).
    summary_recipients: SummaryRecipientsMode | None = None
    owner_id: uuid.UUID | None = None
    # Относительно ТЕКУЩЕГО пользователя запроса (не обязательно владелец).
    is_owner: bool = False
    organizer_name: str | None = None
    participants: list[InviteeOut] = Field(default_factory=list)

    @field_serializer("scheduled_at", "created_at", "next_occurrence")
    def _serialize_utc_z(self, value: datetime | None) -> str | None:
        return _to_iso_z(value) if value is not None else None


class OccurrenceOut(BaseModel):
    """Одно вхождение конференции (закреплённой с повторением или разовой) в календаре."""

    conference_id: uuid.UUID
    title: str | None
    starts_at: datetime
    ends_at: datetime
    number: str
    slug: str
    is_pinned: bool
    is_closed: bool

    @field_serializer("starts_at", "ends_at")
    def _serialize_utc_z(self, value: datetime) -> str:
        return _to_iso_z(value)


class ResolveOut(BaseModel):
    """Публичное представление конференции по номеру/ссылке (экран входа, без auth).

    Для завершённой (`status=ended`) конференции `is_closed`/`requires_password`
    намеренно не заполняются (`None`) — вход всё равно невозможен (410 у
    `join`/`guest-join`), а признак закрытости уже неактуален (ADR-001, п.4,
    уточнение резолва).
    """

    id: uuid.UUID
    title: str | None
    status: str
    is_closed: bool | None = None
    requires_password: bool | None = None


class JoinIn(BaseModel):
    """Тело запроса входа зарегистрированного пользователя — пароль закрытой конференции."""

    password: str | None = None


class GuestJoinIn(BaseModel):
    """Тело запроса гостевого входа: представиться (имя обязательно, email — факультативно)."""

    display_name: str = Field(min_length=1, max_length=255)
    email: EmailStr | None = None
    password: str | None = None


class MuteParticipantIn(BaseModel):
    """Тело запроса принудительного мьюта участника организатором (задача B2).

    `identity` — тот же формат, что и `Participant.identity` в LiveKit
    (`str(user_id)` либо `guest:{id}`); клиент берёт его из `useParticipants()`
    LiveKit, не подбирает вручную.
    """

    identity: str = Field(min_length=1)
    source: ForcedMuteSource


class MuteParticipantOut(BaseModel):
    """Ответ на принудительный мьют — `muted=False`, если трек и так не был опубликован."""

    muted: bool
