#!/usr/bin/env python3
"""
Aggregate downloaded gridMET humidity/heat files to county-level metrics.

Expected input layout:
data/gridmet/<year>/sph.nc
data/gridmet/<year>/rmax.nc
data/gridmet/<year>/rmin.nc

Default output:
data/gridmet/county_gridmet_humidity.csv
"""

from __future__ import annotations

import argparse
import csv
import math
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path

import geopandas as gpd
import numpy as np
import pandas as pd
import xarray as xr


STATE_FIPS_TO_ABBR = {
    "01": "AL",
    "04": "AZ",
    "05": "AR",
    "06": "CA",
    "08": "CO",
    "09": "CT",
    "10": "DE",
    "11": "DC",
    "12": "FL",
    "13": "GA",
    "16": "ID",
    "17": "IL",
    "18": "IN",
    "19": "IA",
    "20": "KS",
    "21": "KY",
    "22": "LA",
    "23": "ME",
    "24": "MD",
    "25": "MA",
    "26": "MI",
    "27": "MN",
    "28": "MS",
    "29": "MO",
    "30": "MT",
    "31": "NE",
    "32": "NV",
    "33": "NH",
    "34": "NJ",
    "35": "NM",
    "36": "NY",
    "37": "NC",
    "38": "ND",
    "39": "OH",
    "40": "OK",
    "41": "OR",
    "42": "PA",
    "44": "RI",
    "45": "SC",
    "46": "SD",
    "47": "TN",
    "48": "TX",
    "49": "UT",
    "50": "VT",
    "51": "VA",
    "53": "WA",
    "54": "WV",
    "55": "WI",
    "56": "WY",
}
CONUS_STATE_FIPS = set(STATE_FIPS_TO_ABBR)
REQUIRED_VARIABLES = ("sph", "rmax", "rmin")
SUMMER_MONTHS = {6, 7, 8}
NOAA_REGION_CODE_TO_FIPS_OVERRIDES = {
    "18511": "11001",
}
NOAA_TMAX_SOURCE_FIPS_OVERRIDES = {
    "46113": (
        "46102",
        "Shannon County, SD changed name/code to Oglala Lakota County, SD effective 2015-05-01.",
    ),
    "51515": (
        "51019",
        "Bedford independent city, VA changed to town status and was added to Bedford County effective 2013-07-01.",
    ),
    "51678": (
        "51163",
        "Lexington independent city, VA uses surrounding Rockbridge County, VA as NOAA tmax proxy because NOAA county daily files do not include Lexington city.",
    ),
}


@dataclass(frozen=True)
class CountyGridMap:
    counties: gpd.GeoDataFrame
    pixel_indices: np.ndarray
    county_indices: np.ndarray
    weights: np.ndarray
    sorted_pixel_indices: np.ndarray
    sorted_county_indices: np.ndarray
    sorted_weights: np.ndarray
    group_starts: np.ndarray
    group_county_indices: np.ndarray
    lat_dim: str
    lon_dim: str


@dataclass
class YearData:
    datasets: list[xr.Dataset]
    arrays: dict[str, xr.DataArray]


def _normalize_fips(value: object, width: int = 5) -> str:
    text = str(value).strip()
    digits = "".join(ch for ch in text if ch.isdigit())
    return digits.zfill(width)[-width:] if digits else ""


def _load_state_crosswalk(path: Path) -> dict[str, str]:
    if not path.exists():
        print(
            f"State crosswalk not found at {path}. Falling back to the first two "
            "NOAA region-code digits as state FIPS."
        )
        return {}

    rows = list(csv.reader(path.open("r", encoding="utf-8-sig", newline="")))
    if not rows:
        return {}

    header = [cell.strip().lower() for cell in rows[0]]
    ncei_idx = next((idx for idx, cell in enumerate(header) if "ncei" in cell and "code" in cell), None)
    fips_idx = next((idx for idx, cell in enumerate(header) if "fips" in cell and "code" in cell), None)
    data_rows = rows[1:] if ncei_idx is not None and fips_idx is not None else rows

    mapping: dict[str, str] = {}
    for row in data_rows:
        if ncei_idx is not None and fips_idx is not None:
            if len(row) <= max(ncei_idx, fips_idx):
                continue
            ncei = _normalize_fips(row[ncei_idx], 2)
            fips = _normalize_fips(row[fips_idx], 2)
        else:
            codes = [_normalize_fips(cell, 2) for cell in row]
            codes = [code for code in codes if code]
            if len(codes) < 2:
                continue
            ncei, fips = codes[0], codes[1]
        if ncei and fips:
            mapping[ncei] = fips
    return mapping


def _county_fips_from_noaa_region(region_code: object, state_crosswalk: dict[str, str]) -> str:
    code = _normalize_fips(region_code, 5)
    if code in NOAA_REGION_CODE_TO_FIPS_OVERRIDES:
        return NOAA_REGION_CODE_TO_FIPS_OVERRIDES[code]
    state_fips = state_crosswalk.get(code[:2], code[:2])
    return f"{state_fips}{code[-3:]}" if state_fips else ""


def _load_counties(path: Path) -> gpd.GeoDataFrame:
    counties = gpd.read_file(path)
    if counties.crs is None:
        counties = counties.set_crs("EPSG:4326")
    else:
        counties = counties.to_crs("EPSG:4326")

    if "id" in counties.columns:
        fips = counties["id"]
    elif "GEOID" in counties.columns:
        fips = counties["GEOID"]
    elif "GEOID10" in counties.columns:
        fips = counties["GEOID10"]
    else:
        raise ValueError("County GeoJSON needs an id, GEOID, or GEOID10 column.")

    counties["countyFips"] = fips.map(_normalize_fips)
    counties["stateFips"] = counties["countyFips"].str.slice(0, 2)
    counties = counties[counties["stateFips"].isin(CONUS_STATE_FIPS)].copy()
    counties["countyName"] = counties.get("NAME", counties["countyFips"]).astype(str)
    counties["state"] = counties["stateFips"].map(STATE_FIPS_TO_ABBR)
    counties = counties.sort_values("countyFips").reset_index(drop=True)
    counties["countyIndex"] = np.arange(len(counties), dtype=np.int32)
    return counties


def _find_dimension(dataset: xr.Dataset, candidates: tuple[str, ...]) -> str:
    for candidate in candidates:
        if candidate in dataset.dims or candidate in dataset.coords:
            return candidate
    lower_lookup = {name.lower(): name for name in set(dataset.dims) | set(dataset.coords)}
    for candidate in candidates:
        if candidate.lower() in lower_lookup:
            return lower_lookup[candidate.lower()]
    raise ValueError(f"Unable to find one of these dimensions: {', '.join(candidates)}")


def _select_variable(dataset: xr.Dataset, preferred: str) -> str:
    if preferred in dataset.data_vars:
        return preferred
    for name in dataset.data_vars:
        if name.lower() == preferred.lower():
            return name
    if len(dataset.data_vars) == 1:
        return next(iter(dataset.data_vars))
    raise ValueError(f"Unable to select variable {preferred}; found {list(dataset.data_vars)}")


def _year_folder(path: Path) -> int | None:
    try:
        return int(path.name)
    except ValueError:
        return None


def _discover_complete_years(gridmet_dir: Path) -> list[int]:
    years: list[int] = []
    for child in sorted(gridmet_dir.iterdir()):
        if not child.is_dir():
            continue
        year = _year_folder(child)
        if year is None:
            continue
        if all((child / f"{variable}.nc").exists() for variable in REQUIRED_VARIABLES):
            years.append(year)
    return years


def _load_noaa_tmax_month(
    noaa_tmax_dir: Path,
    year: int,
    month: int,
    state_crosswalk: dict[str, str],
) -> dict[str, list[float]]:
    path = noaa_tmax_dir / str(year) / f"tmax-{year}{month:02d}-cty-scaled.csv"
    if not path.exists():
        return {}

    values: dict[str, list[float]] = {}
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.reader(handle)
        for row in reader:
            if len(row) < 7 or row[0].strip().lower() != "cty":
                continue
            county_fips = _county_fips_from_noaa_region(row[1], state_crosswalk)
            day_values: list[float] = []
            for raw_value in row[6:]:
                try:
                    value = float(raw_value)
                except ValueError:
                    day_values.append(math.nan)
                    continue
                day_values.append(value if value > -999 else math.nan)
            values[county_fips] = day_values
    return values


def _noaa_tmax_for_date(
    *,
    date: pd.Timestamp,
    county_fips: Sequence[str],
    noaa_tmax_dir: Path,
    state_crosswalk: dict[str, str],
    month_cache: dict[tuple[int, int], dict[str, list[float]]],
) -> np.ndarray:
    cache_key = (int(date.year), int(date.month))
    if cache_key not in month_cache:
        month_cache[cache_key] = _load_noaa_tmax_month(
            noaa_tmax_dir,
            int(date.year),
            int(date.month),
            state_crosswalk,
        )

    month_values = month_cache[cache_key]
    result = np.full(len(county_fips), np.nan, dtype=np.float64)
    day_offset = int(date.day) - 1
    for index, fips in enumerate(county_fips):
        source_fips = NOAA_TMAX_SOURCE_FIPS_OVERRIDES.get(fips, (fips, ""))[0]
        values = month_values.get(source_fips)
        if values is not None and day_offset < len(values):
            result[index] = values[day_offset]
    return result


def _build_county_grid_map(counties: gpd.GeoDataFrame, sample_file: Path) -> CountyGridMap:
    with xr.open_dataset(sample_file) as dataset:
        lat_dim = _find_dimension(dataset, ("lat", "latitude", "y"))
        lon_dim = _find_dimension(dataset, ("lon", "longitude", "x"))
        lat_values = np.asarray(dataset[lat_dim].values)
        lon_values = np.asarray(dataset[lon_dim].values)

    if lat_values.ndim != 1 or lon_values.ndim != 1:
        raise ValueError("This script expects 1D latitude and longitude coordinates.")

    lon_grid, lat_grid = np.meshgrid(lon_values, lat_values)
    flat_lats = lat_grid.ravel()
    flat_lons = lon_grid.ravel()
    flat_indices = np.arange(flat_lats.size, dtype=np.int64)

    bounds = counties.total_bounds
    in_bounds = (
        (flat_lons >= bounds[0] - 0.25)
        & (flat_lats >= bounds[1] - 0.25)
        & (flat_lons <= bounds[2] + 0.25)
        & (flat_lats <= bounds[3] + 0.25)
    )
    point_frame = gpd.GeoDataFrame(
        {
            "pixelIndex": flat_indices[in_bounds],
            "lat": flat_lats[in_bounds],
        },
        geometry=gpd.points_from_xy(flat_lons[in_bounds], flat_lats[in_bounds]),
        crs="EPSG:4326",
    )

    joined = gpd.sjoin(
        point_frame,
        counties[["countyIndex", "geometry"]],
        how="inner",
        predicate="within",
    )
    joined = joined.drop_duplicates(subset=["pixelIndex", "countyIndex"])

    pixel_indices = joined["pixelIndex"].to_numpy(dtype=np.int64)
    county_indices = joined["countyIndex"].to_numpy(dtype=np.int32)
    weights = np.cos(np.deg2rad(joined["lat"].to_numpy(dtype=np.float64)))

    mapped_counties = set(county_indices.tolist())
    fallback_pixels: list[int] = []
    fallback_counties: list[int] = []
    fallback_weights: list[float] = []
    for county_index, geometry in enumerate(counties.geometry):
        if county_index in mapped_counties or geometry.is_empty:
            continue
        point = geometry.representative_point()
        lon_position = int(np.argmin(np.abs(lon_values - point.x)))
        lat_position = int(np.argmin(np.abs(lat_values - point.y)))
        flat_index = lat_position * len(lon_values) + lon_position
        fallback_pixels.append(flat_index)
        fallback_counties.append(county_index)
        fallback_weights.append(float(math.cos(math.radians(float(lat_values[lat_position])))))

    if fallback_pixels:
        pixel_indices = np.concatenate([pixel_indices, np.asarray(fallback_pixels, dtype=np.int64)])
        county_indices = np.concatenate([county_indices, np.asarray(fallback_counties, dtype=np.int32)])
        weights = np.concatenate([weights, np.asarray(fallback_weights, dtype=np.float64)])

    print(
        f"mapped {len(np.unique(county_indices)):,} counties to {len(pixel_indices):,} grid cells "
        f"({len(fallback_pixels):,} nearest-cell fallback counties)"
    )
    sort_order = np.argsort(county_indices, kind="stable")
    sorted_county_indices = county_indices[sort_order]
    group_starts = np.r_[0, np.flatnonzero(np.diff(sorted_county_indices)) + 1]
    group_county_indices = sorted_county_indices[group_starts]
    return CountyGridMap(
        counties=counties,
        pixel_indices=pixel_indices,
        county_indices=county_indices,
        weights=weights,
        sorted_pixel_indices=pixel_indices[sort_order],
        sorted_county_indices=sorted_county_indices,
        sorted_weights=weights[sort_order],
        group_starts=group_starts,
        group_county_indices=group_county_indices,
        lat_dim=lat_dim,
        lon_dim=lon_dim,
    )


def _county_means(
    data_array: xr.DataArray,
    time_index: int,
    time_dim: str,
    county_map: CountyGridMap,
) -> np.ndarray:
    data = data_array.isel({time_dim: time_index}).transpose(county_map.lat_dim, county_map.lon_dim)
    flat = np.asarray(data.values, dtype=np.float64).ravel()
    values = flat[county_map.pixel_indices]
    valid = np.isfinite(values)
    county_count = len(county_map.counties)
    sums = np.bincount(
        county_map.county_indices[valid],
        weights=values[valid] * county_map.weights[valid],
        minlength=county_count,
    )
    weight_sums = np.bincount(
        county_map.county_indices[valid],
        weights=county_map.weights[valid],
        minlength=county_count,
    )
    means = np.full(county_count, np.nan, dtype=np.float64)
    np.divide(sums, weight_sums, out=means, where=weight_sums > 0)
    return means


def _county_means_chunk(
    data_array: xr.DataArray,
    start_index: int,
    end_index: int,
    time_dim: str,
    county_map: CountyGridMap,
) -> np.ndarray:
    data = data_array.isel({time_dim: slice(start_index, end_index)}).transpose(
        time_dim,
        county_map.lat_dim,
        county_map.lon_dim,
    )
    raw = np.asarray(data.values, dtype=np.float64)
    selected = raw.reshape(raw.shape[0], -1)[:, county_map.sorted_pixel_indices]
    valid = np.isfinite(selected)

    weighted_values = np.where(valid, selected * county_map.sorted_weights, 0.0)
    weight_values = np.where(valid, county_map.sorted_weights, 0.0)
    sums = np.add.reduceat(weighted_values, county_map.group_starts, axis=1)
    weight_sums = np.add.reduceat(weight_values, county_map.group_starts, axis=1)

    grouped_means = np.full_like(sums, np.nan, dtype=np.float64)
    np.divide(sums, weight_sums, out=grouped_means, where=weight_sums > 0)

    means = np.full((raw.shape[0], len(county_map.counties)), np.nan, dtype=np.float64)
    means[:, county_map.group_county_indices] = grouped_means
    return means


def _heat_index_f(t_f: np.ndarray, rh_pct: np.ndarray) -> np.ndarray:
    rh = np.clip(rh_pct, 0, 100)
    heat_index = (
        -42.379
        + 2.04901523 * t_f
        + 10.14333127 * rh
        - 0.22475541 * t_f * rh
        - 0.00683783 * t_f * t_f
        - 0.05481717 * rh * rh
        + 0.00122874 * t_f * t_f * rh
        + 0.00085282 * t_f * rh * rh
        - 0.00000199 * t_f * t_f * rh * rh
    )

    low_rh_adjustment = ((13 - rh) / 4) * np.sqrt(np.maximum((17 - np.abs(t_f - 95)) / 17, 0))
    high_rh_adjustment = ((rh - 85) / 10) * ((87 - t_f) / 5)
    heat_index = np.where((rh < 13) & (80 <= t_f) & (t_f <= 112), heat_index - low_rh_adjustment, heat_index)
    heat_index = np.where((rh > 85) & (80 <= t_f) & (t_f <= 87), heat_index + high_rh_adjustment, heat_index)
    return np.where(t_f >= 80, heat_index, t_f)


def _time_dimension(data_array: xr.DataArray, lat_dim: str, lon_dim: str) -> str:
    candidates = [dim for dim in data_array.dims if dim not in {lat_dim, lon_dim}]
    if not candidates:
        raise ValueError(f"No time dimension found for {data_array.name}.")
    return candidates[0]


def _open_year_datasets(gridmet_dir: Path, year: int) -> YearData:
    datasets: list[xr.Dataset] = []
    arrays: dict[str, xr.DataArray] = {}
    for variable in REQUIRED_VARIABLES:
        dataset = xr.open_dataset(gridmet_dir / str(year) / f"{variable}.nc")
        datasets.append(dataset)
        variable_name = _select_variable(dataset, variable)
        arrays[variable] = dataset[variable_name]
    return YearData(datasets=datasets, arrays=arrays)


def _close_year_datasets(year_data: YearData) -> None:
    for dataset in year_data.datasets:
        try:
            dataset.close()
        except Exception:
            pass


def _round_or_blank(value: float, digits: int) -> str:
    if not np.isfinite(value):
        return ""
    return str(round(float(value), digits))


def summarize(args: argparse.Namespace) -> None:
    years = args.years or _discover_complete_years(args.gridmet_dir)
    if not years:
        raise FileNotFoundError(
            f"No complete gridMET years found in {args.gridmet_dir}. "
            "Run scripts/download_gridmet_data.py first."
        )

    if args.years is None:
        start = args.start_year if args.start_year is not None else 1991
        end = args.end_year if args.end_year is not None else 2020
        years = [year for year in years if start <= year <= end]
    elif args.start_year is not None or args.end_year is not None:
        start = args.start_year if args.start_year is not None else min(years)
        end = args.end_year if args.end_year is not None else max(years)
        years = [year for year in years if start <= year <= end]

    if not years:
        raise FileNotFoundError("No complete gridMET years matched the requested year filters.")

    sample_file = args.gridmet_dir / str(years[0]) / "sph.nc"
    counties = _load_counties(args.counties_geojson)
    county_map = _build_county_grid_map(counties, sample_file)
    county_count = len(county_map.counties)
    county_fips = county_map.counties["countyFips"].astype(str).tolist()
    state_crosswalk = _load_state_crosswalk(args.state_crosswalk)
    noaa_month_cache: dict[tuple[int, int], dict[str, list[float]]] = {}

    annual_sph_sum = np.zeros(county_count, dtype=np.float64)
    annual_sph_count = np.zeros(county_count, dtype=np.float64)
    summer_sph_sum = np.zeros(county_count, dtype=np.float64)
    summer_sph_count = np.zeros(county_count, dtype=np.float64)
    annual_rh_sum = np.zeros(county_count, dtype=np.float64)
    annual_rh_count = np.zeros(county_count, dtype=np.float64)
    summer_rh_sum = np.zeros(county_count, dtype=np.float64)
    summer_rh_count = np.zeros(county_count, dtype=np.float64)
    heat_day_sum = np.zeros(county_count, dtype=np.float64)
    years_with_heat_data = np.zeros(county_count, dtype=np.float64)

    for year in years:
        print(f"process {year}")
        year_data = _open_year_datasets(args.gridmet_dir, year)
        arrays = year_data.arrays
        try:
            time_dim = _time_dimension(arrays["sph"], county_map.lat_dim, county_map.lon_dim)
            dates = pd.to_datetime(arrays["sph"][time_dim].values)
            yearly_heat_days = np.zeros(county_count, dtype=np.float64)
            yearly_heat_valid_days = np.zeros(county_count, dtype=np.float64)

            for chunk_start in range(0, len(dates), args.chunk_days):
                chunk_end = min(chunk_start + args.chunk_days, len(dates))
                chunk_dates = dates[chunk_start:chunk_end]
                sph_chunk = _county_means_chunk(arrays["sph"], chunk_start, chunk_end, time_dim, county_map) * 1000
                rmax_chunk = _county_means_chunk(arrays["rmax"], chunk_start, chunk_end, time_dim, county_map)
                rmin_chunk = _county_means_chunk(arrays["rmin"], chunk_start, chunk_end, time_dim, county_map)

                for offset, date in enumerate(chunk_dates):
                    sph = sph_chunk[offset]
                    rmax = rmax_chunk[offset]
                    rmin = rmin_chunk[offset]
                    tmax_f = _noaa_tmax_for_date(
                        date=date,
                        county_fips=county_fips,
                        noaa_tmax_dir=args.noaa_tmax_dir,
                        state_crosswalk=state_crosswalk,
                        month_cache=noaa_month_cache,
                    ) * 9 / 5 + 32

                    rh_mean = (rmax + rmin) / 2
                    annual_sph_valid = np.isfinite(sph)
                    annual_rh_valid = np.isfinite(rh_mean)
                    annual_sph_sum[annual_sph_valid] += sph[annual_sph_valid]
                    annual_sph_count[annual_sph_valid] += 1
                    annual_rh_sum[annual_rh_valid] += rh_mean[annual_rh_valid]
                    annual_rh_count[annual_rh_valid] += 1

                    if int(date.month) in SUMMER_MONTHS:
                        summer_sph_sum[annual_sph_valid] += sph[annual_sph_valid]
                        summer_sph_count[annual_sph_valid] += 1
                        summer_rh_sum[annual_rh_valid] += rh_mean[annual_rh_valid]
                        summer_rh_count[annual_rh_valid] += 1

                    heat_valid = np.isfinite(tmax_f) & np.isfinite(rmin)
                    heat_index = _heat_index_f(tmax_f, rmin)
                    yearly_heat_days[heat_valid] += heat_index[heat_valid] >= args.heat_index_threshold_f
                    yearly_heat_valid_days[heat_valid] += 1

            has_heat_year = yearly_heat_valid_days > 0
            heat_day_sum[has_heat_year] += yearly_heat_days[has_heat_year]
            years_with_heat_data[has_heat_year] += 1
        finally:
            _close_year_datasets(year_data)

    avg_sph = np.divide(annual_sph_sum, annual_sph_count, out=np.full(county_count, np.nan), where=annual_sph_count > 0)
    avg_summer_sph = np.divide(
        summer_sph_sum,
        summer_sph_count,
        out=np.full(county_count, np.nan),
        where=summer_sph_count > 0,
    )
    avg_rh = np.divide(annual_rh_sum, annual_rh_count, out=np.full(county_count, np.nan), where=annual_rh_count > 0)
    avg_summer_rh = np.divide(
        summer_rh_sum,
        summer_rh_count,
        out=np.full(county_count, np.nan),
        where=summer_rh_count > 0,
    )
    humid_heat_days = np.divide(
        heat_day_sum,
        years_with_heat_data,
        out=np.full(county_count, np.nan),
        where=years_with_heat_data > 0,
    )
    source_period = f"{min(years)}-{max(years)}" if len(years) > 1 else str(years[0])

    args.output.parent.mkdir(parents=True, exist_ok=True)
    with args.output.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=[
                "countyFips",
                "countyName",
                "state",
                "avgSpecificHumidityGKg",
                "avgSummerSpecificHumidityGKg",
                "avgRelHumidityPct",
                "avgSummerRelHumidityPct",
                "humidHeatDays",
                "humidHeatSourceFips",
                "humidHeatFipsAdjustment",
                "gridmetAnalysisYears",
                "gridmetSource",
            ],
        )
        writer.writeheader()
        for index, county in county_map.counties.iterrows():
            writer.writerow(
                {
                    "countyFips": county["countyFips"],
                    "countyName": county["countyName"],
                    "state": county["state"],
                    "avgSpecificHumidityGKg": _round_or_blank(avg_sph[index], 3),
                    "avgSummerSpecificHumidityGKg": _round_or_blank(avg_summer_sph[index], 3),
                    "avgRelHumidityPct": _round_or_blank(avg_rh[index], 2),
                    "avgSummerRelHumidityPct": _round_or_blank(avg_summer_rh[index], 2),
                    "humidHeatDays": _round_or_blank(humid_heat_days[index], 2),
                    "humidHeatSourceFips": NOAA_TMAX_SOURCE_FIPS_OVERRIDES.get(
                        county["countyFips"],
                        (county["countyFips"], ""),
                    )[0],
                    "humidHeatFipsAdjustment": NOAA_TMAX_SOURCE_FIPS_OVERRIDES.get(
                        county["countyFips"],
                        ("", ""),
                    )[1],
                    "gridmetAnalysisYears": len(years),
                    "gridmetSource": (
                        f"gridmet-{source_period} county-cell-weighted; "
                        "relative-humidity=(rmax+rmin)/2; "
                        "humidHeatDays=noaa-nclimgrid-tmax+gridmet-rmin heat-index-gte-90f"
                    ),
                }
            )

    print(f"wrote {args.output}")


def _parse_years(text: str) -> list[int]:
    years: set[int] = set()
    for part in text.split(","):
        token = part.strip()
        if not token:
            continue
        if "-" in token:
            start_text, end_text = token.split("-", 1)
            years.update(range(int(start_text), int(end_text) + 1))
        else:
            years.add(int(token))
    return sorted(years)


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Summarize downloaded gridMET humidity files into county-level CSV metrics."
    )
    parser.add_argument("--gridmet-dir", type=Path, default=Path("data/gridmet"))
    parser.add_argument("--noaa-tmax-dir", type=Path, default=Path("data/noaa/tmax_cty_scaled"))
    parser.add_argument("--state-crosswalk", type=Path, default=Path("data/noaa/nclimgrid/us-state-codes_ncei-to-fips.csv"))
    parser.add_argument("--counties-geojson", type=Path, default=Path("data/geojson-counties-fips.json"))
    parser.add_argument("--output", type=Path, default=Path("data/gridmet/county_gridmet_humidity.csv"))
    parser.add_argument("--years", type=_parse_years, default=None, help="Optional comma/range years, e.g. 1991-2020.")
    parser.add_argument("--start-year", type=int, default=None)
    parser.add_argument("--end-year", type=int, default=None)
    parser.add_argument("--heat-index-threshold-f", type=float, default=90.0)
    parser.add_argument("--chunk-days", type=int, default=31)
    summarize(parser.parse_args())
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
