"""租户 AI 调用额度校验 — 月度懒计数 + 5h / 7d 滑动窗口"""
import random
from datetime import datetime, timedelta, timezone
from typing import Optional

from fastapi import HTTPException
from sqlalchemy import select, func, delete
from sqlalchemy.ext.asyncio import AsyncSession


def current_month_key() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m")


def check_ai_quota(quota_monthly: Optional[int], used_this_month: int) -> tuple[bool, Optional[str]]:
    """quota_monthly=None 表示不限额"""
    if quota_monthly is None:
        return True, None
    if used_this_month >= quota_monthly:
        return False, f"本月 AI 调用额度已用完（{used_this_month}/{quota_monthly}次），请下月再试或联系平台升级套餐"
    return True, None


async def _check_sliding_window(
    db: AsyncSession, tenant_id: int,
    quota_5h: Optional[int], quota_weekly: Optional[int],
) -> tuple[bool, Optional[str]]:
    """检查 5h 和 7d 滑动窗口，返回 (ok, msg)"""
    from app.core.models.ai_call_log import AiCallLog

    now = datetime.now(timezone.utc)

    if quota_5h is not None:
        cutoff = now - timedelta(hours=5)
        count_5h = (await db.execute(
            select(func.count()).where(
                AiCallLog.tenant_id == tenant_id,
                AiCallLog.called_at >= cutoff,
            )
        )).scalar_one()
        if count_5h >= quota_5h:
            return False, f"近 5 小时 AI 调用已达上限（{count_5h}/{quota_5h}次），请稍后再试"

    if quota_weekly is not None:
        cutoff = now - timedelta(days=7)
        count_week = (await db.execute(
            select(func.count()).where(
                AiCallLog.tenant_id == tenant_id,
                AiCallLog.called_at >= cutoff,
            )
        )).scalar_one()
        if count_week >= quota_weekly:
            return False, f"近 7 天 AI 调用已达上限（{count_week}/{quota_weekly}次），请下周再试或联系平台升级套餐"

    return True, None


async def get_sliding_usage(db: AsyncSession, tenant_id: int) -> dict:
    """返回 5h / 7d 滑动窗口当前用量，供展示用"""
    from app.core.models.ai_call_log import AiCallLog

    now = datetime.now(timezone.utc)
    cutoff_5h = now - timedelta(hours=5)
    cutoff_week = now - timedelta(days=7)

    count_5h = (await db.execute(
        select(func.count()).where(
            AiCallLog.tenant_id == tenant_id,
            AiCallLog.called_at >= cutoff_5h,
        )
    )).scalar_one()

    count_week = (await db.execute(
        select(func.count()).where(
            AiCallLog.tenant_id == tenant_id,
            AiCallLog.called_at >= cutoff_week,
        )
    )).scalar_one()

    return {"used_5h": count_5h, "used_weekly": count_week}


async def consume_ai_quota(db: AsyncSession, tenant_id: int) -> None:
    """校验并累加一次调用计数；超额抛 429。

    检查顺序：月度 → 5h 窗口 → 7d 窗口，任一超额即拦截。
    通过后写入 ai_call_logs 并累加月度计数。

    ponytail: 计数不区分调用是否最终成功（call_ai 报错也照样计数），
    上限是"Key 配错导致连续失败也会烧额度"；如反馈明显不公平，再仿 upload.py:_refund_quota 加失败退款。
    """
    from app.core.models.tenant import Tenant
    from app.core.models.ai_call_log import AiCallLog

    tr = await db.execute(select(Tenant).where(Tenant.id == tenant_id).with_for_update())
    tenant = tr.scalar_one()

    # 月度懒重置
    month = current_month_key()
    if tenant.ai_quota_month != month:
        tenant.ai_used_this_month = 0
        tenant.ai_quota_month = month

    # 月度检查
    ok, msg = check_ai_quota(tenant.ai_quota_monthly, tenant.ai_used_this_month)
    if not ok:
        await db.commit()
        raise HTTPException(status_code=429, detail=msg)

    # 滑动窗口检查
    ok, msg = await _check_sliding_window(
        db, tenant_id, tenant.ai_quota_5h, tenant.ai_quota_weekly,
    )
    if not ok:
        await db.commit()
        raise HTTPException(status_code=429, detail=msg)

    # 通过：写日志 + 累加月度计数
    db.add(AiCallLog(tenant_id=tenant_id, called_at=datetime.now(timezone.utc)))
    tenant.ai_used_this_month += 1
    await db.commit()

    # ponytail: 概率清理 31 天前的旧日志（约 1% 概率触发，避免每次都 DELETE）
    if random.random() < 0.01:
        cutoff = datetime.now(timezone.utc) - timedelta(days=31)
        await db.execute(
            delete(AiCallLog).where(
                AiCallLog.tenant_id == tenant_id,
                AiCallLog.called_at < cutoff,
            )
        )
        await db.commit()
