"""RequestAuditMiddleware — пишет `api_request` / `admin_action` (+ дедуплицированный
`login`/`login_failed`) события в `user_events` для каждого аутентифицированного
`/api/*` запроса.

Foundation для Feature 2 (login/IP audit) и базы Feature 3 (behavior analytics).
Логирование выполняется ПОСЛЕ `call_next` (не задерживает и не ветвит реальный
ответ клиенту) и через fire-and-forget `schedule_event` — сбой аудита никогда
не влияет на HTTP-ответ.

Порядок middleware-стека (см. `app/main.py`: `rbac_guard` — `@app.middleware("http")`,
объявлен ДО `app.add_middleware(RequestAuditMiddleware)`) делает `RequestAudit`
ВНЕШНИМ по отношению к `rbac_guard` (Starlette строит стек в обратном порядке
регистрации — последний `add_middleware` оборачивает предыдущие). Поэтому к моменту,
когда код ниже читает `response.status_code`, в нём уже отражён исход rbac_guard
(401/403 short-circuit) ИЛИ реального хендлера — статус несёт реальный смысл
"успех/отказ", а не только "запрос дошёл до хендлера".

Заведомо неаутентифицированный трафик (сканеры, долбящиеся в /wp-login.php и т.п.
без валидного basic_auth) сюда вообще не попадает: Caddy гейтит basic_auth ПЕРЕД
проксированием, так что `X-Authenticated-User` в таких запросах нет — условие
`if username and ...` ниже их уже отсекает. Поэтому шум сканеров не нужно
дополнительно фильтровать в этом файле — тот класс проблемы («сигнал тонет в шуме
сканера») здесь структурно невозможен: событие может появиться только для
запроса, прошедшего Caddy basic_auth.
"""

from __future__ import annotations

import logging

from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

from app.core.ratelimit import _client_ip
from app.services.user_events import schedule_event, should_log_login

logger = logging.getLogger(__name__)

# Зеркалит app.main._PUBLIC_PATHS. Не импортируем напрямую из app.main — оно
# импортирует этот модуль (регистрирует middleware), обратный импорт дал бы
# циклическую зависимость.
_PUBLIC_PATHS = frozenset({"/health", "/docs", "/redoc", "/openapi.json"})

# Методы, меняющие состояние — для /api/v1/admin/* именно они должны попадать в
# аудит с атрибуцией (кто именно загрузил куки / включил авто-логин / поправил
# прокси / изменил настройки скрапера / выполнил bulk-операцию). GET/HEAD/OPTIONS
# на /admin/* остаются вне аудита (см. комментарий ниже — это просмотр дашбордов,
# не действие).
_MUTATING_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"})


class RequestAuditMiddleware(BaseHTTPMiddleware):
    """Логирует активность аутентифицированных пользователей в `user_events`."""

    async def dispatch(self, request: Request, call_next):  # type: ignore[no-untyped-def]
        response: Response = await call_next(request)

        try:
            username = request.headers.get("x-authenticated-user")
            path = request.url.path
            if username and path.startswith("/api/") and path not in _PUBLIC_PATHS:
                ip = _client_ip(request)
                ua = request.headers.get("user-agent")
                method = request.method
                success = response.status_code < 400
                is_admin_path = path.startswith("/api/v1/admin/")

                # Общий behavior/activity-поток — каждый authenticated API-запрос.
                # /api/v1/admin/* исключаем из `api_request`: это ops-действия
                # (просмотр дашбордов аудита/аналитики), а не поведение пилота —
                # иначе запросы дашборда зашумляют top_paths и счётчики активности.
                if not is_admin_path:
                    schedule_event(
                        event_type="api_request",
                        username=username,
                        ip=ip,
                        user_agent=ua,
                        path=path,
                        method=method,
                        payload={"status_code": response.status_code},
                    )
                elif method in _MUTATING_METHODS:
                    # Admin-аудит (security-audit fix): раньше ЛЮБОЙ запрос под
                    # /api/v1/admin/* (включая меняющие состояние — загрузка кук,
                    # авто-логин, правка прокси, настройки скраперов, bulk-операции)
                    # полностью исключался из `user_events` тем же условием, что и
                    # шумные GET-дашборды — установить, КТО совершил действие, было
                    # невозможно. Пишем факт действия + атрибуцию (username/ip/path/
                    # method/статус) — БЕЗ тела запроса (там куки/пароли/секреты
                    # правки прокси), это НЕ payload-лог, а событие "что произошло".
                    schedule_event(
                        event_type="admin_action",
                        username=username,
                        ip=ip,
                        user_agent=ua,
                        path=path,
                        method=method,
                        payload={"status_code": response.status_code, "success": success},
                    )

                # Дедуплицированный login/IP-audit сигнал — максимум раз в день
                # на (юзер, IP, устройство). Security-audit fix: раньше событие
                # всегда писалось как `login` независимо от исхода запроса —
                # отражённая RBAC-попытка (валидный Caddy basic_auth, но
                # 401/403 от rbac_guard: протухший X-Internal-Auth-Secret,
                # неизвестная роль, scope-блок) была неотличима от настоящего
                # входа. Теперь тип события расходится по `response.status_code`:
                # `login` — успех, `login_failed` — otказ. Дедуп-бакет (once per
                # user+ip+ua+day) НЕ разбит отдельно на success/fail (это
                # потребовало бы менять `should_log_login` в user_events.py —
                # вне scope этого фикса): если в рамках одного дня с этого же
                # устройства сначала случился отказ, а затем реальный успешный
                # вход, второе событие в тот же день не запишется — тот же
                # компромисс дедупа, что был и раньше, разница только в том, что
                # теперь ЕДИНСТВЕННОЕ событие дня корректно отражает, чем оно было.
                if should_log_login(username, ip, ua):
                    schedule_event(
                        event_type="login" if success else "login_failed",
                        username=username,
                        ip=ip,
                        user_agent=ua,
                        path=path,
                        method=method,
                        payload={"status_code": response.status_code},
                    )
        except Exception:
            logger.warning("RequestAuditMiddleware: failed to record event", exc_info=True)

        return response
