"""Timezone utilities — single source of truth for local time conversion.

All internal timestamps remain UTC. This module provides helpers to convert
UTC → user's local timezone for display and schedule interpretation.

The user's timezone is resolved from config (``schedule.timezone``).
When empty, falls back to the system's local timezone via ``tzlocal``,
then to UTC as a last resort.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime, tzinfo
from functools import lru_cache

try:
    from zoneinfo import ZoneInfo
    _HAS_ZONEINFO = True
except ImportError:
    _HAS_ZONEINFO = False

log = logging.getLogger(__name__)

# ── Module-level configured timezone ──
# Set once at startup via ``configure(tz_name)`` from main.py.
# All helpers read from this; never mutated after startup except in tests.
_configured_tz: str = ""


def configure(tz_name: str) -> None:
    """Set the app-wide timezone from config. Called once at startup."""
    global _configured_tz  # noqa: PLW0603
    _configured_tz = tz_name
    # Invalidate cached resolver
    _resolve_tz.cache_clear()


@lru_cache(maxsize=1)
def _resolve_tz() -> tzinfo:
    """Resolve the effective timezone. Cached after first call."""

    def _try_zoneinfo(name: str) -> tzinfo | None:
        """Try to create a ZoneInfo; return None if tzdata is missing."""
        if not _HAS_ZONEINFO:
            return None
        try:
            return ZoneInfo(name)
        except Exception:
            return None

    # 1. Explicit config
    if _configured_tz:
        tz = _try_zoneinfo(_configured_tz)
        if tz:
            return tz
        log.warning("Invalid timezone '%s' in config, falling back to auto-detect", _configured_tz)

    # 2. System local timezone via TZ env var (IANA name)
    import os
    tz_env = os.environ.get("TZ", "")
    if tz_env and "/" in tz_env:
        tz = _try_zoneinfo(tz_env)
        if tz:
            return tz

    # 3. Try UTC via ZoneInfo
    tz = _try_zoneinfo("UTC")
    if tz:
        return tz

    # 4. Bare stdlib UTC fallback (no tzdata package installed)
    return UTC


def get_tz() -> tzinfo:
    """Return the effective user timezone."""
    return _resolve_tz()


def get_tz_name() -> str:
    """Return the IANA name of the effective timezone (e.g. 'Europe/Stockholm')."""
    tz = get_tz()
    name = str(tz)
    # timezone.utc stringifies to "UTC" which is fine, but ZoneInfo
    # stringifies as the IANA key. Both are acceptable.
    return name if name != "UTC+00:00" else "UTC"


def now_local() -> datetime:
    """Return the current time in the user's local timezone (tz-aware)."""
    return datetime.now(UTC).astimezone(get_tz())


def to_local(dt: datetime) -> datetime:
    """Convert a UTC (or any tz-aware) datetime to the user's local timezone.

    Naive datetimes are assumed to be UTC.
    """
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=UTC)
    return dt.astimezone(get_tz())


def local_today() -> str:
    """Return today's date string (YYYY-MM-DD) in the user's local timezone.

    This is the correct boundary for journal files, daily memory, etc.
    """
    return now_local().strftime("%Y-%m-%d")


def local_hour() -> int:
    """Return the current hour (0-23) in the user's local timezone.

    Used by the daily cycle to decide morning/evening triggers.
    """
    return now_local().hour


def local_weekday() -> int:
    """Return the current weekday (0=Monday, 6=Sunday) in the user's local timezone."""
    return now_local().weekday()


def format_local(dt: datetime, fmt: str = "%Y-%m-%d %H:%M") -> str:
    """Format a datetime in the user's local timezone."""
    return to_local(dt).strftime(fmt)


def format_iso_local(dt: datetime) -> str:
    """Format a datetime as ISO 8601 with the local timezone offset.

    Example: '2026-04-17T09:30:00+02:00'
    """
    return to_local(dt).isoformat()
