"""
Date utilities for month/day ranges (no year).

Provides parsing and range-checking functions for month/day pairs
(e.g., (6,21) meaning June 21).

Functions:
- parse_month_day: parse '6/21', '06-21', '2026-06-21', 'June 21', 'Jun 21'
- is_month_day_in_range: test whether a month/day lies within a start/end pair
- month_day_to_day_of_year: convert a month/day to day-of-year for comparisons
- format_month_day: display nicely

Note: Uses a fixed non-leap reference year for day-of-year calculations.
"""

import re
import calendar
from datetime import date
from typing import Tuple

# Reference non-leap year used for day-of-year calculations
_REFERENCE_YEAR = 2001

_MONTH_NAME_MAP = {
    'jan': 1,
    'january': 1,
    'feb': 2,
    'february': 2,
    'mar': 3,
    'march': 3,
    'apr': 4,
    'april': 4,
    'may': 5,
    'jun': 6,
    'june': 6,
    'jul': 7,
    'july': 7,
    'aug': 8,
    'august': 8,
    'sep': 9,
    'sept': 9,
    'september': 9,
    'oct': 10,
    'october': 10,
    'nov': 11,
    'november': 11,
    'dec': 12,
    'december': 12,
}


def month_day_to_day_of_year(month: int, day: int) -> int:
    """Convert (month, day) to day-of-year using a non-leap reference year."""
    try:
        dt = date(_REFERENCE_YEAR, month, day)
    except ValueError as e:
        raise ValueError(f"Invalid month/day: {month}/{day}: {e}")
    return dt.timetuple().tm_yday


def is_month_day_in_range(
    month: int, day: int, start: Tuple[int, int], end: Tuple[int, int]
) -> bool:
    """Return True if (month, day) is within the inclusive range start->end.

    Handles ranges that cross year boundary (e.g., Dec 20 -> Jan 5).
    """
    day_num = month_day_to_day_of_year(month, day)
    start_num = month_day_to_day_of_year(*start)
    end_num = month_day_to_day_of_year(*end)

    if start_num <= end_num:
        return start_num <= day_num <= end_num
    # Wrap-around
    return day_num >= start_num or day_num <= end_num


def parse_month_day(s: str) -> Tuple[int, int]:
    """Parse a date string and return (month, day).

    Accepted formats:
      - MM/DD or M/D
      - MM-DD
      - YYYY-MM-DD or YYYY/MM/DD (year ignored)
      - MonthName D or Mon D (e.g., 'June 21' or 'Jun 21')

    Raises ValueError if cannot parse or the date is invalid.
    """
    if not s or not isinstance(s, str):
        raise ValueError("Empty date string")

    s = s.strip()

    # Numeric forms: MM/DD or MM-DD
    m = re.match(r"^(?P<m>\d{1,2})\s*[/-]\s*(?P<d>\d{1,2})$", s)
    if m:
        month = int(m.group("m"))
        day = int(m.group("d"))
        # validate
        month_day_to_day_of_year(month, day)
        return month, day

    # ISO with year: YYYY-MM-DD or YYYY/MM/DD
    m = re.match(r"^(?P<y>\d{4})[/-](?P<m>\d{1,2})[/-](?P<d>\d{1,2})$", s)
    if m:
        month = int(m.group("m"))
        day = int(m.group("d"))
        month_day_to_day_of_year(month, day)
        return month, day

    # Month name forms: 'June 21' or '21 June'
    parts = re.split(r"[ ,]+", s)
    if len(parts) == 2:
        a, b = parts
        # If a is month name
        a_low = a.lower()
        if a_low in _MONTH_NAME_MAP:
            month = _MONTH_NAME_MAP[a_low]
            day = int(b)
            month_day_to_day_of_year(month, day)
            return month, day
        # If b is month name (e.g., '21 June')
        b_low = b.lower()
        if b_low in _MONTH_NAME_MAP:
            month = _MONTH_NAME_MAP[b_low]
            day = int(a)
            month_day_to_day_of_year(month, day)
            return month, day

    raise ValueError(f"Unrecognized date format: '{s}'")


def format_month_day(month: int, day: int) -> str:
    """Return a short human-friendly representation like 'Jun 21'."""
    if not (1 <= month <= 12):
        raise ValueError("month must be 1..12")
    # Use abbreviated month name
    month_name = calendar.month_abbr[month]
    return f"{month_name} {day}"