# backend/app/plugins/advanced_stats/timerange.py
"""时间范围解析与时区偏移。

时区方案：用固定偏移量算术，不用 MySQL 的 CONVERT_TZ——后者依赖
mysql.time_zone_name 时区表，很多部署没跑过 mysql_tzinfo_to_sql，
未加载时函数返回 NULL，报表会莫名全空。

偏移量由前端传入（-new Date().getTimezoneOffset()），浏览器给的值
已经包含当前夏令时状态。

# ponytail: 整个查询区间共用一个 tz_offset。跨夏令时切换的区间，切换点
# 当天会差 1 小时。日/月报表可忽略；要精确到小时且跨 DST 时再换 CONVERT_TZ
# （需先在 MySQL 加载时区表）。
"""
from __future__ import annotations

from datetime import date, datetime, timedelta

# UTC-14:00 到 UTC+14:00，覆盖所有真实时区
TZ_MIN_MINUTES = -840
TZ_MAX_MINUTES = 840

QUICK_RANGES: tuple[str, ...] = (
    "custom",
    "today", "yesterday",
    "week", "month", "quarter", "year",
    "current_week", "current_month", "current_quarter", "current_year",
    "last_week", "last_month", "last_quarter", "last_year",
    "all_time",
)


def validate_tz_offset(minutes: int) -> int:
    if not TZ_MIN_MINUTES <= minutes <= TZ_MAX_MINUTES:
        raise ValueError(f"时区偏移必须在 [{TZ_MIN_MINUTES}, {TZ_MAX_MINUTES}] 分钟内，收到 {minutes}")
    return minutes


def _quarter_start(d: date) -> date:
    return date(d.year, ((d.month - 1) // 3) * 3 + 1, 1)


def resolve_range(
    range_key: str,
    *,
    today: date,
    date_start: date | None = None,
    date_end: date | None = None,
) -> tuple[date | None, date]:
    """把范围 key 解析成 (开始日期, 结束日期)，均为**本地日期**、闭区间。

    开始日期为 None 表示不设下界（all_time）。
    today 由调用方按用户时区算出后传入，本函数不读系统时钟——这样才能测。
    """
    if range_key not in QUICK_RANGES:
        raise ValueError(f"未知的时间范围: {range_key!r}")

    if range_key == "custom":
        if date_start is None or date_end is None:
            raise ValueError("range=custom 时必须同时提供 date_start 和 date_end")
        return date_start, date_end

    if range_key == "all_time":
        return None, today
    if range_key == "today":
        return today, today
    if range_key == "yesterday":
        y = today - timedelta(days=1)
        return y, y

    # 滚动窗口（含今天）
    if range_key == "week":
        return today - timedelta(days=6), today
    if range_key == "month":
        return today - timedelta(days=29), today
    if range_key == "quarter":
        return today - timedelta(days=90), today
    if range_key == "year":
        return today - timedelta(days=364), today

    # 本期（期初到今天）
    if range_key == "current_week":
        return today - timedelta(days=today.weekday()), today
    if range_key == "current_month":
        return today.replace(day=1), today
    if range_key == "current_quarter":
        return _quarter_start(today), today
    if range_key == "current_year":
        return date(today.year, 1, 1), today

    # 上期（完整周期）
    if range_key == "last_week":
        this_monday = today - timedelta(days=today.weekday())
        last_monday = this_monday - timedelta(days=7)
        return last_monday, last_monday + timedelta(days=6)
    if range_key == "last_month":
        first_this = today.replace(day=1)
        last_day_prev = first_this - timedelta(days=1)
        return last_day_prev.replace(day=1), last_day_prev
    if range_key == "last_quarter":
        q_start = _quarter_start(today)
        prev_q_end = q_start - timedelta(days=1)
        return _quarter_start(prev_q_end), prev_q_end
    if range_key == "last_year":
        return date(today.year - 1, 1, 1), date(today.year - 1, 12, 31)


def local_range_to_utc_bounds(
    date_start: date | None,
    date_end: date,
    tz_offset_minutes: int,
) -> tuple[datetime | None, datetime]:
    """把本地日期闭区间换算成 UTC 时间戳半开区间 [lo, hi)。

    WHERE 用这个区间，才能走 ix_orders_tenant_status_date 索引；
    GROUP BY 才用偏移后的日期切天（见 dimensions.shift）。

    右端取 date_end 的**次日**零点，半开区间——用闭区间会漏掉当天
    23:59:59 之后到零点之间的订单。
    """
    validate_tz_offset(tz_offset_minutes)
    delta = timedelta(minutes=tz_offset_minutes)
    lo = None if date_start is None else datetime.combine(date_start, datetime.min.time()) - delta
    hi = datetime.combine(date_end + timedelta(days=1), datetime.min.time()) - delta
    return lo, hi


def today_in_tz(*, now_utc: datetime, tz_offset_minutes: int) -> date:
    """按用户时区算出"今天"是哪天。调用方传入已视为"UTC 瞬时"的 naive datetime。"""
    validate_tz_offset(tz_offset_minutes)
    return (now_utc + timedelta(minutes=tz_offset_minutes)).date()
