"""
共享的时间解析与时区工具。

统一处理：
- 本地业务时区下的“今天/今年”
- 本地日期 + 时间组合
- offset-less ISO 时间按业务时区解释
- 跨午夜结束时间
"""

from __future__ import annotations

from datetime import datetime, timedelta

from compat import ZoneInfo, make_aware

_DEFAULT_TZ = "Asia/Shanghai"


class DateTimeValidationError(ValueError):
    """时间输入不合法。"""


def now_local(tz: str = _DEFAULT_TZ) -> datetime:
    return datetime.now(ZoneInfo(tz))


def current_date_str(tz: str = _DEFAULT_TZ) -> str:
    return now_local(tz).strftime("%Y-%m-%d")


def current_year(tz: str = _DEFAULT_TZ) -> int:
    return now_local(tz).year


def parse_local_date(date_str: str, tz: str = _DEFAULT_TZ) -> datetime:
    try:
        dt = datetime.strptime(date_str.strip(), "%Y-%m-%d")
    except ValueError as exc:
        raise DateTimeValidationError(
            f"日期格式错误：'{date_str}'，应为 YYYY-MM-DD"
        ) from exc
    return make_aware(dt, tz)


def parse_local_time(time_str: str, base_date: datetime) -> datetime:
    raw = time_str.strip()
    for fmt in ("%H:%M", "%H:%M:%S"):
        try:
            parsed = datetime.strptime(raw, fmt)
            return base_date.replace(
                hour=parsed.hour,
                minute=parsed.minute,
                second=0,
                microsecond=0,
            )
        except ValueError:
            continue
    raise DateTimeValidationError(f"时间格式错误：'{time_str}'，应为 HH:MM")


def build_event_range(
    date_str: str,
    start_str: str,
    end_str: str | None = None,
    duration_minutes: int = 60,
    tz: str = _DEFAULT_TZ,
) -> tuple[datetime, datetime]:
    if duration_minutes <= 0:
        raise DateTimeValidationError("持续时间必须大于 0 分钟")

    base = parse_local_date(date_str, tz)
    start_dt = parse_local_time(start_str, base)

    if end_str:
        end_dt = parse_local_time(end_str, base)
        if end_dt <= start_dt:
            end_dt = end_dt + timedelta(days=1)
    else:
        end_dt = start_dt + timedelta(minutes=duration_minutes)

    return start_dt, end_dt


def parse_iso_datetime(
    value: str,
    tz: str = _DEFAULT_TZ,
    assume_local_if_naive: bool = True,
) -> datetime:
    raw = value.strip()
    if not raw:
        raise DateTimeValidationError("时间不能为空")

    try:
        dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
    except ValueError as exc:
        raise DateTimeValidationError(
            f"时间格式错误：'{value}'，应为 ISO 8601"
        ) from exc

    if dt.tzinfo is None:
        if not assume_local_if_naive:
            raise DateTimeValidationError(
                f"时间缺少时区偏移：'{value}'，请使用 ISO 8601 带时区格式"
            )
        return make_aware(dt, tz)

    return dt


def local_date_window(date_str: str, tz: str = _DEFAULT_TZ) -> tuple[datetime, datetime]:
    start = parse_local_date(date_str, tz)
    return start, start + timedelta(days=1)


def local_date_label(dt: datetime, tz: str = _DEFAULT_TZ) -> str:
    return dt.astimezone(ZoneInfo(tz)).strftime("%Y-%m-%d")
