#!/usr/bin/env python3
"""One-off script: fetch Fed FOMC pages and print Python literals (run manually)."""

from __future__ import annotations

import re
import urllib.request
from calendar import monthrange
from datetime import date, timedelta

UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"


def fetch(url: str) -> str:
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=45) as r:
        return r.read().decode("utf-8", errors="replace")


_MONTHS = {
    "January": 1,
    "February": 2,
    "March": 3,
    "April": 4,
    "May": 5,
    "June": 6,
    "July": 7,
    "August": 8,
    "September": 9,
    "October": 10,
    "November": 11,
    "December": 12,
}


def _parse_day_cell(raw: str) -> tuple[int, int | None] | None:
    """Return (last_day, first_day_if_span) or None if skip."""
    s = raw.strip()
    s = s.replace("*", "")
    if "(" in s:
        m = re.match(r"^(\d+)\s*\(", s)
        if m:
            return int(m.group(1)), None
        return None
    if re.match(r"^\d+-\d+$", s):
        a, b = s.split("-")
        return int(b), int(a)
    if re.match(r"^\d+$", s):
        return int(s), None
    return None


def _resolve_calendar_date(
    year: int, month_label: str, day_cell: str
) -> date | None:
    parsed = _parse_day_cell(day_cell)
    if parsed is None:
        return None
    last_d, first_d = parsed
    ml = month_label.strip()

    if "/" in ml:
        parts = ml.split("/")
        if len(parts) != 2:
            return None
        m1, m2 = parts[0].strip(), parts[1].strip()
        # Apr/May 30-1 -> May 1
        if m1 in ("Apr", "April") and m2 in ("May",):
            return date(year, 5, last_d)
        if m1 in ("Jan", "January") and m2 in ("Feb", "February"):
            return date(year, 2, last_d)
        if m1 in ("Oct", "October") and m2 in ("Nov", "November"):
            return date(year, 11, last_d)
        # expand abbrev
        abb = {"Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6, "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12}
        if m1 in abb and m2 in abb:
            return date(year, abb[m2], last_d)
        return None

    if ml not in _MONTHS:
        return None
    mo = _MONTHS[ml]
    # validate day
    _, n_days = monthrange(year, mo)
    if last_d > n_days or last_d < 1:
        return None
    return date(year, mo, last_d)


def parse_fomccalendars(html: str, max_year: int | None = None) -> list[date]:
    out: list[date] = []
    parts = re.split(r'<h4><a[^>]*>(\d{4}) FOMC Meetings</a></h4>', html)
    # parts[0] = preamble; then (year, block) pairs
    i = 1
    while i + 1 < len(parts):
        year = int(parts[i])
        block = parts[i + 1]
        i += 2
        if max_year is not None and year > max_year:
            continue
        for chunk in re.split(
            r'<div class="(?:fomc-meeting--shaded )?row fomc-meeting"', block
        ):
            if "fomc-meeting__month" not in chunk:
                continue
            mm = re.search(
                r'fomc-meeting__month[^>]*>\s*<strong>([^<]+)</strong>', chunk
            )
            dd = re.search(r'fomc-meeting__date[^>]*>([^<]+)', chunk)
            if not mm or not dd:
                continue
            d = _resolve_calendar_date(year, mm.group(1), dd.group(1))
            if d:
                out.append(d)
    return out


def parse_fomchistorical(html: str, year: int) -> list[date]:
    """Parse Fed fomchistoricalYYYY.htm; policy day = last day of scheduled meeting."""
    out: list[date] = []
    for hm in re.finditer(r"<h5[^>]*>([^<]+)</h5>", html):
        title = hm.group(1).strip()
        if "Meeting -" not in title:
            continue
        # July 31-August 1 Meeting - 2012
        m = re.match(
            r"^([A-Za-z]+) (\d+)-([A-Za-z]+) (\d+)\s+Meeting - (\d{4})\s*$", title
        )
        if m:
            y = int(m.group(5))
            if y != year:
                continue
            m2_name, d2 = m.group(3), int(m.group(4))
            if m2_name not in _MONTHS:
                continue
            mo = _MONTHS[m2_name]
            _, n_days = monthrange(y, mo)
            if 1 <= d2 <= n_days:
                out.append(date(y, mo, d2))
            continue

        m = re.match(
            r"^([A-Za-z]+(?:/[A-Za-z]+)?) (\d+(?:-\d+)?)"
            r"(?: \(unscheduled\))?(?: \(cancelled\))?\s+Meeting - (\d{4})\s*$",
            title,
        )
        if not m:
            continue
        mon_label, daypart, y = m.group(1), m.group(2), int(m.group(3))
        if y != year:
            continue
        if "(cancelled)" in title:
            continue
        d = _resolve_calendar_date(y, mon_label, daypart)
        if d:
            out.append(d)
    return out


def federal_election_day(year: int) -> date:
    """First Tuesday after first Monday in November (US federal general election day)."""
    first_monday: date | None = None
    for day in range(1, 8):
        d = date(year, 11, day)
        if d.weekday() == 0:
            first_monday = d
            break
    assert first_monday is not None
    return first_monday + timedelta(days=1)


def federal_election_days(start_year: int, end_year: int) -> list[date]:
    """Presidential + midterm years: US federal general is every even year."""
    out: list[date] = []
    y = start_year if start_year % 2 == 0 else start_year + 1
    while y <= end_year:
        out.append(federal_election_day(y))
        y += 2
    return out


def first_friday_of_month(year: int, month: int) -> date:
    d = date(year, month, 1)
    # Friday = 4
    days_ahead = (4 - d.weekday()) % 7
    return date(year, month, 1 + days_ahead)


def employment_situation_dates(start_year: int, end_year: int) -> list[date]:
    """BLS Employment Situation: almost always first Friday of the month."""
    out: list[date] = []
    for y in range(start_year, end_year + 1):
        for m in range(1, 13):
            out.append(first_friday_of_month(y, m))
    return out


def cpi_release_dates_approx(start_year: int, end_year: int) -> list[date]:
    """
    BLS CPI: typically second Tuesday or Wednesday of the month (varies).
    Use second Tuesday — close to typical schedule; not identical to every release.
    """
    out: list[date] = []
    for y in range(start_year, end_year + 1):
        for m in range(1, 13):
            d = date(y, m, 1)
            # second Tuesday: find first Tuesday then +7
            days_to_tue = (1 - d.weekday()) % 7
            first_tue = 1 + days_to_tue
            second_tue = first_tue + 7
            out.append(date(y, m, second_tue))
    return out


def main() -> None:
    fomc: list[date] = []
    for y in range(2006, 2021):
        url = f"https://www.federalreserve.gov/monetarypolicy/fomchistorical{y}.htm"
        try:
            html = fetch(url)
        except Exception as e:
            print(f"# ERROR {y}: {e}", flush=True)
            continue
        fomc.extend(parse_fomchistorical(html, y))

    cal_html = fetch("https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm")
    fomc.extend(parse_fomccalendars(cal_html, max_year=2025))

    fomc = sorted({d for d in fomc if d.year <= 2025})

    el = federal_election_days(2006, 2025)
    nfp = employment_situation_dates(2006, 2025)
    cpi = cpi_release_dates_approx(2006, 2025)

    all_d = sorted(set(fomc + el + nfp + cpi))
    print(f"# FOMC unique: {len(fomc)}, elections: {len(el)}, NFP: {len(nfp)}, CPI: {len(cpi)}, total unique: {len(all_d)}")
    print("FOMC_DATES = (")
    for d in fomc:
        print(f'    "{d.isoformat()}",')
    print(")")
    print("ALL_MACRO_DATES = (")
    for d in all_d:
        print(f'    "{d.isoformat()}",')
    print(")")


if __name__ == "__main__":
    main()
