"""Роутер администрирования: конференции, пользователи, команды, настройки.

Все эндпоинты требуют роль `admin` (`Depends(require_admin)`, 403 иначе).
Правки конференций/удаление переиспользуют `ConferenceService` (тот же
бизнес-слой, что и обычный роутер конференций) — админ проходит проверку
владения как «или владелец, или админ» (см. `ConferenceService._ensure_owner_or_admin`).
Рассылка приглашений и вся отправка писем — только в Celery-задачах; здесь
лишь постановка в очередь и немедленный ответ `202`.
Справочник команд (`teams`) — простой CRUD без бизнес-правил, кроме
уникальности названия; привязка пользователя к команде — `users.team_id`
(`ON DELETE SET NULL`).
"""

import uuid
from pathlib import Path
from typing import Annotated, Literal

import anyio
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession

from api.deps import require_admin

# Алиас обязателен: ниже в этом модуле уже есть роутер-хендлер `get_settings`
# (`GET /admin/settings`) — без переименования он затирает имя импортированной
# функции в globals модуля (последнее связывание имени побеждает).
from core.config import get_settings as get_app_settings
from core.db import get_session
from core.plugins.config import InstanceConfig
from core.security import hash_password
from models.conference import Conference
from models.user import User
from repositories.admin import AdminConferenceRepository, AdminUserRepository, TeamRepository
from repositories.users import UserRepository
from schemas.admin import (
    AdminConferenceListOut,
    AdminConferenceOut,
    AdminUserCreateIn,
    AdminUserListOut,
    AdminUserOut,
    AdminUserUpdateIn,
    InvitationsSendIn,
    SettingsOut,
    TeamCreateIn,
    TeamListOut,
    TeamOut,
    TeamUpdateIn,
    TestEmailIn,
    TestEmailOut,
)
from schemas.conferences import ConferenceUpdateIn
from services.ai_levels import detect_ai_levels
from services.avatars import AvatarInvalidTypeError, AvatarTooLargeError, avatar_url
from services.conferences import (
    ConferenceActiveError,
    ConferenceNotFoundError,
    ConferenceService,
    InvalidConferenceStateError,
    NotConferenceOwnerError,
)
from services.email import EmailSendError, create_email_backend
from services.instance_settings import (
    InstanceSettingsService,
    InvalidAiLevelError,
    InvalidConsentPolicyError,
    InvalidContactEmailError,
    InvalidEmailDomainError,
    InvalidTimezoneError,
    SettingsUpdateIn,
)
from services.invitations_producer import enqueue_invitations
from services.pipeline_producer import transcription_queue_served
from services.profile import resolve_team_name, set_avatar

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

DEFAULT_LIMIT = 50
MAX_LIMIT = 200


# --- Конференции ------------------------------------------------------------------


@router.get("/conferences", response_model=AdminConferenceListOut)
async def list_conferences(
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
    status_filter: Annotated[str | None, Query(alias="status")] = None,
    q: Annotated[str | None, Query()] = None,
    limit: Annotated[int, Query(gt=0, le=MAX_LIMIT)] = DEFAULT_LIMIT,
    offset: Annotated[int, Query(ge=0)] = 0,
) -> AdminConferenceListOut:
    """Список всех конференций инстанса с фильтром по статусу и текстовым поиском."""
    rows, total = await AdminConferenceRepository(session).list_paginated(
        status=status_filter, q=q, limit=limit, offset=offset
    )
    service = ConferenceService(session)
    items = [
        _to_admin_conference_out(
            service, conference, viewer_id=admin.id, owner_name=owner_name, owner_email=owner_email
        )
        for conference, owner_name, owner_email in rows
    ]
    return AdminConferenceListOut(items=items, total=total)


@router.patch("/conferences/{conference_id}", response_model=AdminConferenceOut)
async def update_conference(
    conference_id: uuid.UUID,
    data: ConferenceUpdateIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> AdminConferenceOut:
    """Изменить любую конференцию инстанса (реюз `ConferenceService.update`)."""
    service = ConferenceService(session)
    try:
        conference = await service.update(conference_id, actor=admin, 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:  # недостижимо для admin, оставлено для полноты
        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
    owner_name, owner_email = await _load_owner(session, conference)
    return _to_admin_conference_out(
        service, conference, viewer_id=admin.id, owner_name=owner_name, owner_email=owner_email
    )


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


@router.post(
    "/conferences/{conference_id}/invitations",
    status_code=status.HTTP_202_ACCEPTED,
)
async def send_conference_invitations(
    conference_id: uuid.UUID,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
    data: InvitationsSendIn = InvitationsSendIn(),
) -> None:
    """Поставить в очередь ручную рассылку .ics-приглашений (отправка — только в Celery)."""
    conference = await session.get(Conference, conference_id)
    if conference is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found")
    enqueue_invitations(conference_id, emails=data.emails)


# --- Пользователи ------------------------------------------------------------------


@router.get("/users", response_model=AdminUserListOut)
async def list_users(
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
    status: Annotated[Literal["active", "blocked"] | None, Query()] = None,
    q: Annotated[str | None, Query()] = None,
    limit: Annotated[int, Query(gt=0, le=MAX_LIMIT)] = DEFAULT_LIMIT,
    offset: Annotated[int, Query(ge=0)] = 0,
) -> AdminUserListOut:
    """Список пользователей инстанса — фильтр по статусу (`active`/`blocked`, без
    параметра — все) и текстовый поиск по email/имени/названию команды."""
    rows, total = await AdminUserRepository(session).list_paginated(
        status=status, q=q, limit=limit, offset=offset
    )
    media_root = _media_root()
    items = [
        _to_admin_user_out(user, team_name=team_name, media_root=media_root)
        for user, team_name in rows
    ]
    return AdminUserListOut(items=items, total=total)


@router.post("/users", response_model=AdminUserOut, status_code=status.HTTP_201_CREATED)
async def create_user(
    data: AdminUserCreateIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> AdminUserOut:
    """Создать пользователя от имени администратора.

    В отличие от самостоятельной регистрации (`POST /auth/register`),
    email сразу считается подтверждённым (`email_verified=True`) — письмо с
    подтверждением не отправляется; роль по умолчанию — `user`. Дубль email —
    409 `email_already_registered` (тот же код, что у публичной регистрации);
    несуществующая команда — 404 `team_not_found`.
    """
    repo = UserRepository(session)
    if await repo.get_by_email(data.email) is not None:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="email_already_registered")
    if data.team_id is not None:
        team = await TeamRepository(session).get(data.team_id)
        if team is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="team_not_found")

    user = await repo.create(
        email=data.email,
        name_user=data.name_user,
        password_hash=await hash_password(data.password),
        team_id=data.team_id,
    )
    user.email_verified = True
    await session.commit()
    team_name = await resolve_team_name(session, user.team_id)
    return _to_admin_user_out(user, team_name=team_name, media_root=_media_root())


@router.get("/users/{user_id}", response_model=AdminUserOut)
async def get_user(
    user_id: uuid.UUID,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> AdminUserOut:
    """Карточка профиля пользователя — те же данные, что в своём профиле."""
    row = await AdminUserRepository(session).get_with_team(user_id)
    if row is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
    user, team_name = row
    return _to_admin_user_out(user, team_name=team_name, media_root=_media_root())


@router.patch("/users/{user_id}", response_model=AdminUserOut)
async def update_user(
    user_id: uuid.UUID,
    data: AdminUserUpdateIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> AdminUserOut:
    """Изменить роль/блокировку/ФИО/команду пользователя.

    Запрет самоизменения (409) распространяется только на `role`/`is_blocked` —
    своё ФИО/команду админ менять может (та же карточка).
    """
    if user_id == admin.id and (data.role is not None or data.is_blocked is not None):
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="cannot_modify_self")

    user = await session.get(User, user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")

    if data.role is not None:
        user.role = data.role
    if data.is_blocked is not None:
        user.is_blocked = data.is_blocked
    if data.name_user is not None:
        user.name_user = data.name_user
    if "team_id" in data.model_fields_set:
        # Явная передача (в т.ч. `null`) — назначить/снять команду; отсутствие
        # поля в запросе значение не трогает (тот же паттерн, что
        # `summary_recipients` в `services/conferences.py`).
        if data.team_id is not None:
            team = await TeamRepository(session).get(data.team_id)
            if team is None:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="team_not_found")
        user.team_id = data.team_id
    await session.commit()
    team_name = await resolve_team_name(session, user.team_id)
    return _to_admin_user_out(user, team_name=team_name, media_root=_media_root())


@router.post("/users/{user_id}/avatar", response_model=AdminUserOut)
async def upload_user_avatar(
    user_id: uuid.UUID,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
    file: Annotated[UploadFile, File()],
) -> AdminUserOut:
    """Загрузить аватар любому пользователю (та же валидация, что `POST /users/me/avatar`)."""
    user = await session.get(User, user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
    try:
        await set_avatar(_media_root(), user, file)
    except AvatarTooLargeError as exc:
        raise HTTPException(
            status_code=status.HTTP_413_CONTENT_TOO_LARGE, detail="avatar_too_large"
        ) from exc
    except AvatarInvalidTypeError as exc:
        raise HTTPException(
            status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail="avatar_invalid_type"
        ) from exc
    await session.commit()
    team_name = await resolve_team_name(session, user.team_id)
    return _to_admin_user_out(user, team_name=team_name, media_root=_media_root())


# --- Команды ------------------------------------------------------------------------


@router.get("/teams", response_model=TeamListOut)
async def list_teams(
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> TeamListOut:
    """Список всех команд, отсортированный по названию."""
    items, total = await TeamRepository(session).list_all()
    return TeamListOut(items=[TeamOut.model_validate(team) for team in items], total=total)


@router.post("/teams", response_model=TeamOut, status_code=status.HTTP_201_CREATED)
async def create_team(
    data: TeamCreateIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> TeamOut:
    """Создать команду; дубль названия (регистрозависимо) — 409."""
    repo = TeamRepository(session)
    if await repo.get_by_name(data.name) is not None:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="team_name_taken")
    team = await repo.create(data.name)
    await session.commit()
    return TeamOut.model_validate(team)


@router.patch("/teams/{team_id}", response_model=TeamOut)
async def update_team(
    team_id: uuid.UUID,
    data: TeamUpdateIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> TeamOut:
    """Переименовать команду; нет команды — 404, дубль названия — 409."""
    repo = TeamRepository(session)
    team = await repo.get(team_id)
    if team is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="team_not_found")

    existing = await repo.get_by_name(data.name)
    if existing is not None and existing.id != team_id:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="team_name_taken")

    team.name = data.name
    await session.commit()
    return TeamOut.model_validate(team)


@router.delete("/teams/{team_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_team(
    team_id: uuid.UUID,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> None:
    """Удалить команду (у пользователей `team_id` обнулится, ON DELETE SET NULL); нет — 404."""
    repo = TeamRepository(session)
    team = await repo.get(team_id)
    if team is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="team_not_found")
    await repo.delete(team)
    await session.commit()


# --- Настройки инстанса -------------------------------------------------------------


@router.get("/settings", response_model=SettingsOut)
async def get_settings(
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> SettingsOut:
    """Текущие эффективные настройки инстанса.

    `transcription_queue_served` вычисляется блокирующим вызовом Celery
    (`app.control.inspect`, ждёт ответа брокера/воркеров) — выносится в поток
    через `anyio.to_thread.run_sync`, чтобы не блокировать event loop.
    """
    cfg = await InstanceSettingsService(session).get()
    queue_served = await anyio.to_thread.run_sync(transcription_queue_served)
    return _to_settings_out(cfg, transcription_queue_served=queue_served)


@router.put("/settings", response_model=SettingsOut)
async def update_settings(
    data: SettingsUpdateIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> SettingsOut:
    """Частично обновить настройки инстанса; недоступный уровень AI/таймзона/домен — 400."""
    service = InstanceSettingsService(session)
    try:
        cfg = await service.update(data)
    except (
        InvalidAiLevelError,
        InvalidTimezoneError,
        InvalidEmailDomainError,
        InvalidContactEmailError,
        InvalidConsentPolicyError,
    ) as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
    queue_served = await anyio.to_thread.run_sync(transcription_queue_served)
    return _to_settings_out(cfg, transcription_queue_served=queue_served)


@router.post("/settings/test-email", response_model=TestEmailOut)
async def send_test_email(
    data: TestEmailIn,
    admin: Annotated[User, Depends(require_admin)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> TestEmailOut:
    """Отправить тестовое письмо синхронно — проверка почтовой конфигурации без
    регистрации фиктивного пользователя.

    Получатель по умолчанию — email текущего администратора. Отправка идёт
    напрямую из эндпоинта (не через Celery), чтобы результат был виден сразу.
    Секреты SMTP (логин/пароль) в ответе не участвуют — только хост/порт,
    и то лишь при `EMAIL_BACKEND=smtp` (см. `.env.example`).
    """
    app_settings = get_app_settings()
    recipient = data.to or admin.email
    cfg = await InstanceSettingsService(session).get()
    reply_to = cfg.contact_email if cfg.contact_email_enabled else None
    is_smtp = app_settings.email_backend == "smtp"
    smtp_host = app_settings.smtp_host if is_smtp else None
    smtp_port = app_settings.smtp_port if is_smtp else None

    backend = create_email_backend(app_settings)
    try:
        await backend.send(
            to=recipient,
            subject="Тестовое письмо VidConf",
            body="Это тестовое письмо для проверки почтовой конфигурации инстанса VidConf.",
            reply_to=reply_to,
        )
    except EmailSendError as exc:
        return TestEmailOut(
            success=False, message=str(exc), smtp_host=smtp_host, smtp_port=smtp_port
        )
    return TestEmailOut(
        success=True,
        message=f"Письмо успешно отправлено на {recipient}",
        smtp_host=smtp_host,
        smtp_port=smtp_port,
    )


def _to_settings_out(cfg: InstanceConfig, *, transcription_queue_served: bool) -> SettingsOut:
    """Собрать `SettingsOut` из эффективной конфигурации + доступность уровней AI."""
    return SettingsOut(
        chat_enabled=cfg.chat.enabled,
        hand_queue_enabled=cfg.hand_queue.enabled,
        transcription_enabled=cfg.transcriber.enabled,
        ai_level=cfg.ai_level,
        ai_levels=detect_ai_levels(cfg),
        transcription_queue_served=transcription_queue_served,
        summary_recipients=cfg.summary_recipients,
        display_timezone=cfg.display_timezone,
        registration_team_choice=cfg.registration_team_choice,
        registration_email_domain_enabled=cfg.registration_email_domain_enabled,
        registration_email_domains=cfg.registration_email_domains,
        contact_email_enabled=cfg.contact_email_enabled,
        contact_email=cfg.contact_email,
        publish_quality_cap=cfg.media_limits.publish_quality_cap,
        stage_max_tiles=cfg.media_limits.stage_max_tiles,
        consent_required=cfg.consent_required,
        consent_policy_text=cfg.consent_policy_text,
        consent_policy_version=cfg.consent_policy_version,
        device_check_enabled=cfg.device_check_enabled,
        virtual_background_enabled=cfg.virtual_background_enabled,
    )


def _to_admin_conference_out(
    service: ConferenceService,
    conference: Conference,
    *,
    viewer_id: uuid.UUID,
    owner_name: str | None,
    owner_email: str | None,
) -> AdminConferenceOut:
    """Дополнить `ConferenceOut` данными владельца для админ-таблицы конференций.

    `participants` намеренно не заполняется — та же логика, что у `/my`
    (список не раздувает состав, ADR-003, п.5); `organizer_name` переиспользует
    уже загруженное здесь имя владельца (`owner_name`) — повторного запроса не нужно.
    """
    base = service.to_out(conference, viewer_id=viewer_id, organizer_name=owner_name)
    return AdminConferenceOut(**base.model_dump(), owner_name=owner_name, owner_email=owner_email)


async def _load_owner(
    session: AsyncSession, conference: Conference
) -> tuple[str | None, str | None]:
    """Имя/email владельца конференции (`None`/`None`, если владельца нет — ADR-001)."""
    if conference.owner_id is None:
        return None, None
    owner = await session.get(User, conference.owner_id)
    if owner is None:
        return None, None
    return owner.name_user, owner.email


def _media_root() -> Path:
    """Каталог загруженных медиа-файлов (см. `core/config.py::Settings.media_root`)."""
    return Path(get_app_settings().media_root)


def _to_admin_user_out(user: User, *, team_name: str | None, media_root: Path) -> AdminUserOut:
    """Собрать `AdminUserOut` — та же карточка, что и `UserProfileOut`, + модерация."""
    return AdminUserOut(
        id=user.id,
        email=user.email,
        name_user=user.name_user,
        role=user.role,
        is_blocked=user.is_blocked,
        email_verified=user.email_verified,
        created_at=user.created_at,
        team_id=user.team_id,
        avatar_url=avatar_url(media_root, user.avatar_path),
        team_name=team_name,
    )
