"""Сервис квоты оценок trade-in — N успешных оценок в месяц на аккаунт.

Правила:
- Лимит по умолчанию = settings.estimate_quota_limit успешных оценок за календарный
  месяц (UTC, период 'YYYY-MM'); конфигурируется через env ESTIMATE_QUOTA_LIMIT,
  default 15.
- Персональный override: таблица account_quota_overrides (username → monthly_limit),
  см. миграцию 185_account_quota_overrides.sql. Заменяет прежний хак бонусных попыток
  через negative `used` (ломал /quota — «Осталось 50 из 15»).
- `used` в account_estimate_usage защищён CHECK (used >= 0) на уровне схемы, см.
  миграцию 189_account_estimate_usage_nonnegative.sql — 185 сбросила негативный
  used только для user2, 189 закрывает остальные аккаунты + запрещает регресс.
  В коде декремента `used` НЕТ — increment() только `used + 1` под TOCTOU-guard
  (#747); любой negative used приходит исключительно извне (ручной UPDATE).
- Без лимита (unlimited): роль admin (без похода в БД) ИЛИ персональный грант
  account_quota_overrides.unlimited = true (миграция 191_account_quota_unlimited_flag.sql).
  До миграции 191 unlimited для non-admin аккаунтов был захардкожен как
  `username == 'kopylov'` прямо в коде — данные (kopylov + praktika) заменяют этот
  хардкод целиком, единый источник правды для всех безлимитных non-admin грантов.
- Учитываются ТОЛЬКО успешные оценки (инкремент ПОСЛЕ estimate_quality).
- Если заголовок X-Authenticated-User отсутствует (dev без Caddy) → unlimited,
  лимит не применяется (fail-open).
- При исчерпании лимита поднимается HTTPException(429).
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime

from fastapi import HTTPException
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.core.auth import get_role
from app.core.config import settings

logger = logging.getLogger(__name__)

# Лимит успешных оценок за календарный месяц — конфигурируется через
# env ESTIMATE_QUOTA_LIMIT (core.config.Settings), default 15 (#658).
MONTHLY_LIMIT = settings.estimate_quota_limit
LIMIT_EXHAUSTED_MESSAGE = (
    f"Лимит из {MONTHLY_LIMIT} оценок в этом месяце исчерпан. "
    "За полной версией обращайтесь к Копылову."
)


def current_period() -> str:
    """Возвращает текущий период в формате 'YYYY-MM' (UTC)."""
    return datetime.now(UTC).strftime("%Y-%m")


def is_unlimited(db: Session, username: str) -> bool:
    """True если пользователь не ограничен квотой.

    Unlimited если:
      - роль admin (RBAC roles.yaml, in-memory, БЕЗ похода в БД — admin гарантированно
        безлимитен по дизайну RBAC, отдельная per-user запись не нужна);
      - ЛИБО персональный грант account_quota_overrides.unlimited = true (миграция
        191) — единственный источник правды для non-admin безлимитных аккаунтов,
        включая kopylov (перенесён сюда этой же миграцией, до 191 был захардкожен
        как `username == 'kopylov'`) и praktika (пилот восстановлен 2026-07-27).

    KeyError (неизвестный пользователь, не в roles.yaml) → трактуется как limited
    (False), БЕЗ похода в БД — override-таблица не источник правды для юзеров,
    которых вообще нет в RBAC-конфиге.
    """
    try:
        role = get_role(username)
    except KeyError:
        return False
    if role == "admin":
        return True
    row = db.execute(
        text(
            """
            SELECT unlimited FROM account_quota_overrides
            WHERE username = :u
            """
        ),
        {"u": username},
    ).fetchone()
    return bool(row is not None and row.unlimited)


def user_limit(db: Session, username: str) -> int:
    """Персональный месячный лимит для username, иначе глобальный MONTHLY_LIMIT.

    Источник override — таблица account_quota_overrides (см. миграцию
    185_account_quota_overrides.sql). Заменяет прежний хак бонусных попыток через
    negative `used`, который ломал /quota (limit=15, used=-35 → remaining=50 —
    «Осталось 50 из 15»).
    """
    row = db.execute(
        text(
            """
            SELECT monthly_limit FROM account_quota_overrides
            WHERE username = :u
            """
        ),
        {"u": username},
    ).fetchone()
    if row is not None and row.monthly_limit is not None:
        return int(row.monthly_limit)
    return MONTHLY_LIMIT


def get_status(db: Session, username: str | None) -> dict:
    """Возвращает статус квоты для пользователя.

    Если username is None → unlimited True, used 0, remaining = MONTHLY_LIMIT.
    Если unlimited → used = фактический или 0, remaining = limit (per-user override
    или глобальный MONTHLY_LIMIT).
    """
    if username is None:
        return {
            "limit": MONTHLY_LIMIT,
            "used": 0,
            "remaining": MONTHLY_LIMIT,
            "unlimited": True,
        }

    unlimited = is_unlimited(db, username)
    period = current_period()
    limit = user_limit(db, username)

    row = db.execute(
        text(
            """
            SELECT used FROM account_estimate_usage
            WHERE username = :u AND period_month = :p
            """
        ),
        {"u": username, "p": period},
    ).fetchone()

    used = row.used if row is not None else 0

    if unlimited:
        return {
            "limit": limit,
            "used": used,
            "remaining": limit,
            "unlimited": True,
        }

    # Защитный кламп: remaining никогда не превышает limit, даже если used всё же
    # снова просочится отрицательным (прежний бонус-хак) — max(0, used) обнуляет
    # отрицательный used перед вычитанием.
    remaining = max(0, limit - max(0, used))
    return {
        "limit": limit,
        "used": used,
        "remaining": remaining,
        "unlimited": False,
    }


def check_and_raise(db: Session, username: str | None) -> None:
    """Проверяет лимит квоты и поднимает 429 если исчерпан.

    Если username is None или пользователь unlimited → no-op.
    """
    if username is None:
        return

    if is_unlimited(db, username):
        return

    period = current_period()
    limit = user_limit(db, username)
    row = db.execute(
        text(
            """
            SELECT used FROM account_estimate_usage
            WHERE username = :u AND period_month = :p
            """
        ),
        {"u": username, "p": period},
    ).fetchone()

    used = row.used if row is not None else 0
    if used >= limit:
        logger.warning(
            "quota exhausted: username=%r period=%s used=%d limit=%d",
            username,
            period,
            used,
            limit,
        )
        raise HTTPException(status_code=429, detail=LIMIT_EXHAUSTED_MESSAGE)


def increment(db: Session, username: str | None) -> bool:
    """Атомарно-условный инкремент счётчика успешных оценок (#747).

    Возвращает True если инкремент успешен; False если лимит исчерпан.
    None / unlimited → True (no-op success).

    Защита от TOCTOU: предикат `WHERE used < :lim` применяется к ветке DO UPDATE —
    два параллельных запроса при used=lim-1 не могут оба инкрементировать (второй
    упрётся в WHERE → RETURNING пуст → False). Свежая вставка (used=1) НЕ задевается
    WHERE (он только для DO UPDATE), поэтому первая оценка месяца проходит. `lim` —
    персональный лимит (user_limit), НЕ жёстко зашитый глобальный MONTHLY_LIMIT.
    """
    if username is None or is_unlimited(db, username):
        return True

    period = current_period()
    lim = user_limit(db, username)
    row = db.execute(
        text(
            """
            INSERT INTO account_estimate_usage (username, period_month, used, updated_at)
            VALUES (:u, :p, 1, NOW())
            ON CONFLICT (username, period_month)
            DO UPDATE SET
                used = account_estimate_usage.used + 1,
                updated_at = NOW()
            WHERE account_estimate_usage.used < :lim
            RETURNING used
            """
        ),
        {"u": username, "p": period, "lim": lim},
    ).fetchone()
    db.commit()
    ok = row is not None
    if ok:
        logger.debug("quota incremented: username=%r period=%s used=%s", username, period, row[0])
    else:
        logger.warning(
            "quota increment refused (atomic, #747): username=%r period=%s limit=%d",
            username,
            period,
            lim,
        )
    return ok
