from __future__ import annotations

from datetime import datetime, timedelta
import logging
from zoneinfo import ZoneInfo

from app.services.redis_client import get_redis_client


logger = logging.getLogger("app.traffic")
LOCAL_TIMEZONE = ZoneInfo("Asia/Shanghai")
GRAIN_FORMATS = {
    "minute": "%Y%m%d%H%M",
    "hour": "%Y%m%d%H",
    "day": "%Y%m%d",
    "week": "%G-W%V",
}
GRAIN_LABELS = {
    "minute": "%Y-%m-%d %H:%M",
    "hour": "%Y-%m-%d %H:00",
    "day": "%Y-%m-%d",
    "week": "%G 年第 %V 周",
}
RETENTION_SECONDS = 60 * 60 * 24 * 8
HISTORY_DAYS = 7
EXCLUDED_PATHS = {"/api/health", "/api/ready", "/api/admin/dashboard/traffic"}


class RequestTrafficService:
    @staticmethod
    async def record(path: str, status_code: int, duration_ms: float, now: datetime | None = None) -> None:
        if not path.startswith("/api/") or path in EXCLUDED_PATHS:
            return
        redis = get_redis_client()
        if redis is None:
            return
        current = (now or datetime.now(LOCAL_TIMEZONE)).astimezone(LOCAL_TIMEZONE)
        try:
            key = f"metrics:http:minute:{current.strftime(GRAIN_FORMATS['minute'])}"
            async with redis.pipeline(transaction=False) as pipeline:
                pipeline.hincrby(key, "requests", 1)
                pipeline.hincrby(key, "errors", 1 if status_code >= 500 else 0)
                pipeline.hincrbyfloat(key, "duration_ms", max(duration_ms, 0))
                pipeline.expire(key, RETENTION_SECONDS)
                await pipeline.execute()
        except Exception:
            logger.warning("request_traffic_record_failed", exc_info=True)

    @staticmethod
    async def peak_traffic(grain: str) -> dict:
        if grain not in GRAIN_FORMATS:
            raise ValueError("不支持的时间粒度")
        redis = get_redis_client()
        empty = {"grain": grain, "peakPeriod": None, "peakRequests": 0, "totalRequests": 0, "rows": []}
        if redis is None:
            return empty
        try:
            keys = [key async for key in redis.scan_iter(match="metrics:http:minute:*", count=500)]
            if not keys:
                return empty
            cutoff = datetime.now(LOCAL_TIMEZONE) - timedelta(days=HISTORY_DAYS)
            prefix = "metrics:http:minute:"
            parsed_keys = []
            expired_keys = []
            for key in keys:
                try:
                    minute = datetime.strptime(key.removeprefix(prefix), GRAIN_FORMATS["minute"]).replace(tzinfo=LOCAL_TIMEZONE)
                except ValueError:
                    continue
                if minute >= cutoff:
                    parsed_keys.append((key, minute))
                else:
                    expired_keys.append(key)
            parsed_keys.sort(key=lambda item: item[1])
            async with redis.pipeline(transaction=False) as retention_pipeline:
                for key in expired_keys:
                    retention_pipeline.unlink(key)
                for key, minute in parsed_keys:
                    retention_pipeline.expireat(key, int((minute + timedelta(seconds=RETENTION_SECONDS)).timestamp()))
                await retention_pipeline.execute()
            if not parsed_keys:
                return empty
            async with redis.pipeline(transaction=False) as pipeline:
                for key, _minute in parsed_keys:
                    pipeline.hgetall(key)
                values = await pipeline.execute()
        except Exception:
            logger.warning("request_traffic_query_failed", exc_info=True)
            return empty

        buckets: dict[str, dict[str, float]] = {}
        for (_key, minute), value in zip(parsed_keys, values, strict=False):
            request_count = int(float(value.get("requests", 0)))
            if request_count <= 0:
                continue
            error_count = int(float(value.get("errors", 0)))
            duration_ms = float(value.get("duration_ms", 0))
            period = minute.strftime(GRAIN_FORMATS[grain])
            bucket = buckets.setdefault(period, {"requests": 0, "errors": 0, "duration_ms": 0})
            bucket["requests"] += request_count
            bucket["errors"] += error_count
            bucket["duration_ms"] += duration_ms

        rows = []
        for period in _period_sequence(grain, cutoff, datetime.now(LOCAL_TIMEZONE)):
            bucket = buckets.get(period, {"requests": 0, "errors": 0, "duration_ms": 0})
            request_count = int(bucket["requests"])
            error_count = int(bucket["errors"])
            rows.append(
                {
                    "period": period,
                    "periodLabel": _format_period(grain, period),
                    "requestCount": request_count,
                    "errorCount": error_count,
                    "errorRate": round(error_count / request_count * 100, 2) if request_count else 0,
                    "avgResponseMs": round(bucket["duration_ms"] / request_count, 2) if request_count else 0,
                }
            )
        peak = max(rows, key=lambda item: item["requestCount"], default=None)
        return {
            "grain": grain,
            "peakPeriod": peak["periodLabel"] if peak else None,
            "peakRequests": peak["requestCount"] if peak else 0,
            "totalRequests": sum(row["requestCount"] for row in rows),
            "rows": rows,
        }


def _format_period(grain: str, value: str) -> str:
    if grain == "week":
        year, week = value.split("-W", 1)
        return f"{year} 年第 {week} 周"
    parsed = datetime.strptime(value, GRAIN_FORMATS[grain])
    return parsed.strftime(GRAIN_LABELS[grain])


def _period_sequence(grain: str, start: datetime, end: datetime) -> list[str]:
    if grain == "minute":
        current, step = start.replace(second=0, microsecond=0), timedelta(minutes=1)
    elif grain == "hour":
        current, step = start.replace(minute=0, second=0, microsecond=0), timedelta(hours=1)
    elif grain == "day":
        current, step = start.replace(hour=0, minute=0, second=0, microsecond=0), timedelta(days=1)
    elif grain == "week":
        current = start.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=start.weekday())
        step = timedelta(days=7)
    else:
        raise ValueError("不支持的时间粒度")
    periods = []
    while current <= end:
        periods.append(current.strftime(GRAIN_FORMATS[grain]))
        current += step
    return periods
