"""Date and ISO helpers for duty ranges and API validation."""

import re
from datetime import date, datetime, timedelta, timezone
from typing import Literal


def day_start_iso(d: date) -> str:
    """ISO 8601 start of calendar day UTC: YYYY-MM-DDT00:00:00Z."""
    return d.isoformat() + "T00:00:00Z"


def day_end_iso(d: date) -> str:
    """ISO 8601 end of calendar day UTC: YYYY-MM-DDT23:59:59Z."""
    return d.isoformat() + "T23:59:59Z"


def duty_to_iso(d: date, hour_utc: int, minute_utc: int) -> str:
    """ISO 8601 with Z for start of duty on date d at given UTC time."""
    dt = datetime(d.year, d.month, d.day, hour_utc, minute_utc, 0, tzinfo=timezone.utc)
    return dt.strftime("%Y-%m-%dT%H:%M:%SZ")


_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


# Maximum allowed date range in days (e.g. 731 = 2 years).
MAX_DATE_RANGE_DAYS = 731


class DateRangeValidationError(ValueError):
    """Raised when from_date/to_date validation fails. API uses kind for i18n key."""

    def __init__(
        self,
        kind: Literal["bad_format", "from_after_to", "range_too_large"],
    ) -> None:
        self.kind = kind
        super().__init__(kind)


def to_date_exclusive_iso(to_date: str) -> str:
    """Return the day after to_date in YYYY-MM-DD for exclusive range end.

    Use for queries like start_at < to_date_next (e.g. filter end before next day).

    Args:
        to_date: End date in YYYY-MM-DD.

    Returns:
        (to_date + 1 day) in YYYY-MM-DD.
    """
    dt = datetime.fromisoformat(to_date + "T00:00:00") + timedelta(days=1)
    return dt.strftime("%Y-%m-%d")


def parse_utc_iso(iso_str: str) -> datetime:
    """Parse UTC ISO 8601 string with Z suffix to timezone-aware datetime (UTC).

    Args:
        iso_str: e.g. '2025-01-15T09:00:00Z'.

    Returns:
        Timezone-aware datetime in UTC.
    """
    normalized = (iso_str or "").strip().replace("Z", "+00:00")
    return datetime.fromisoformat(normalized)


def parse_utc_iso_naive(iso_str: str) -> datetime:
    """Parse UTC ISO 8601 string with Z to naive datetime in UTC.

    Use for job queue or code that expects naive UTC.

    Args:
        iso_str: e.g. '2025-01-15T09:00:00Z'.

    Returns:
        Naive datetime with same UTC wall time.
    """
    dt = parse_utc_iso(iso_str)
    return dt.replace(tzinfo=None)


def parse_iso_date(s: str) -> date | None:
    """Parse YYYY-MM-DD string to date. Returns None if invalid."""
    if not s or not _ISO_DATE_RE.match(s.strip()):
        return None
    try:
        return date.fromisoformat(s.strip())
    except ValueError:
        return None


def validate_date_range(from_date: str, to_date: str) -> None:
    """Validate from_date and to_date are YYYY-MM-DD, from_date <= to_date, and range <= MAX_DATE_RANGE_DAYS.

    Raises:
        DateRangeValidationError: bad_format if format invalid, from_after_to if from > to,
            range_too_large if (to_date - from_date) > MAX_DATE_RANGE_DAYS.
    """
    if not _ISO_DATE_RE.match(from_date or "") or not _ISO_DATE_RE.match(to_date or ""):
        raise DateRangeValidationError("bad_format")
    if from_date > to_date:
        raise DateRangeValidationError("from_after_to")
    try:
        from_d = date.fromisoformat(from_date)
        to_d = date.fromisoformat(to_date)
    except ValueError:
        raise DateRangeValidationError("bad_format") from None
    if (to_d - from_d).days > MAX_DATE_RANGE_DAYS:
        raise DateRangeValidationError("range_too_large")
