#!/usr/bin/env python3
"""
Site Grid — Faz 2: indirilen ERA5-Land'den hücre başına tek satır üret.

İNDİS KURALI (kullanıcı direktifi 2026-09-04):
    "İndirme maliyetini ARTIRAN hiçbir indisi hesaplama."

Bu betik YALNIZ 01_download_era5.py'nin indirdiği iki üründen BEDAVA çıkanları
hesaplar. Ek indirme isteyen indisler HESAPLANMAZ; hangileri olduğu ve neden
gerektiği README'de listelidir.

BEDAVA (bu indirmeden çıkar):
    tavg[12]         aylık ortalama sıcaklık            <- aylık ürün
    prec[12]         aylık toplam yağış (mm)            <- aylık ürün
    tmin[12]         aylık ortalama günlük minimum      <- günlük minimum
    abs_min          kayıttaki en düşük günlük minimum  <- günlük minimum
    mean_temp        yıllık ortalama sıcaklık           <- tavg türevi
    annual_rain      yıllık toplam yağış                <- prec türevi
    frost_days       Tmin<0 gün sayısı (yıllık ort.)    <- günlük minimum
    last_spring_frost_doy / first_autumn_frost_doy      <- günlük minimum
    frost_free_days  iki don arası gün sayısı           <- günlük minimum
    de_martonne      kuraklık indeksi P/(T+10)          <- tavg+prec türevi
    driest_quarter_mm en kurak 3 ardışık ayın toplamı   <- prec türevi

HESAPLANMAZ (ek indirme ister):
    tmax[12]         -> daily_maximum ayrı indirme (+%100 günlük hacim)
    GDD (baz 10)     -> günlük ortalama VEYA min+max ister (+%100..200)
    soğuklama saati  -> SAATLİK veri ister (+~2300%); direktifle atlandı

ÇIKTI: out/site_grid_<bbox>_<donem>.csv
    grid_key aynen store.ts `gridKey('climate', ...)` biçimindedir
    ("39.9000:32.9000") ki TS tarafı doğrudan eşleştirebilsin.
"""

from __future__ import annotations

import argparse
import calendar
import sys
from pathlib import Path

from config import (
    COPERNICUS_ATTRIBUTION,
    NORMAL_END_YEAR,
    NORMAL_START_YEAR,
    OUT_DIR,
    OVERLAP_END_YEAR,
    OVERLAP_START_YEAR,
    PILOT_BBOX,
    RAW_DIR,
    TURKEY_BBOX,
    BBox,
)

try:
    import numpy as np
    import pandas as pd
    import xarray as xr
except ImportError:  # pragma: no cover
    print("HATA: numpy/pandas/xarray gerekli. `pip install -r requirements.txt`", file=sys.stderr)
    raise SystemExit(4) from None

KELVIN = 273.15


def grid_key(lat: float, lng: float) -> str:
    """store.ts gridKey('climate', ...) ile BİREBİR aynı biçim."""
    snap = lambda v: f"{round(v / 0.1) * 0.1:.4f}"  # noqa: E731
    return f"{snap(lat)}:{snap(lng)}"


def open_monthly(bbox: BBox) -> xr.Dataset:
    path = RAW_DIR / f"monthly_{bbox.name}.nc"
    if not path.exists():
        raise SystemExit(f"HATA: {path} yok. Önce 01_download_era5.py çalıştır.")
    return xr.open_dataset(path)


def open_daily(bbox: BBox, y0: int, y1: int, allow_partial: bool = False) -> xr.Dataset:
    """
    Günlük dosyaları açar ve DÖNEM BÜTÜNLÜĞÜNÜ ZORLAR.

    NEDEN GUARD VAR (ölçüldü 2026-09-06): indirme 12/30 yılken bu betik hiçbir
    uyarı vermeden `site_grid_..._1991-2020.csv` üretti. Dosya adı 30 yıllık
    normal olduğunu söylüyordu ama absMin yalnız 1991-2002'den hesaplanmıştı —
    30 yılın gerçek uç soğuğuna göre SICAK YANLI. Böyle bir ızgara don kapısını
    sessizce gevşetir ve zeytin/incir gibi ürünleri yanlışlıkla kurtarır.

    Varsayılan davranış: eksik yıl varsa DUR. Bilinçli kısmi koşu için
    `--partial` gerekir ve çıktı adına `PARTIAL` eklenir.
    """
    files = sorted(RAW_DIR.glob(f"daily_tmin_{bbox.name}_*.nc"))
    files = [f for f in files if y0 <= int(f.stem.split("_")[-1]) <= y1]
    if not files:
        raise SystemExit(f"HATA: {RAW_DIR} içinde günlük dosya yok.")
    have = {int(f.stem.split("_")[-1]) for f in files}
    missing = [y for y in range(y0, y1 + 1) if y not in have]
    if missing:
        msg = (
            f"Günlük veri EKSİK: {len(have)}/{y1 - y0 + 1} yıl var, "
            f"{len(missing)} yıl yok -> {missing}"
        )
        if not allow_partial:
            raise SystemExit(
                f"HATA: {msg}\n"
                f"       Eksik yılla üretilen ızgara '{y0}-{y1}' normali DEĞİLDİR; "
                f"özellikle absMin sıcak yanlı çıkar.\n"
                f"       İndirme bitince tekrar çalıştır ya da bilerek kısmi "
                f"üretim için --partial ver."
            )
        print(f"  UYARI: {msg} (--partial verildi, çıktı PARTIAL olarak adlandırılacak)")
    return xr.open_mfdataset(files, combine="by_coords", chunks={"valid_time": 365})


def _var(ds: xr.Dataset, *names: str) -> xr.DataArray:
    """CDS sürümleri değişken adını değiştirebiliyor; ilk bulunanı al."""
    for n in names:
        if n in ds:
            return ds[n]
    raise SystemExit(f"HATA: {names} değişkenlerinden hiçbiri yok. Mevcut: {list(ds.data_vars)}")


def _time_dim(da: xr.DataArray) -> str:
    for n in ("valid_time", "time", "forecast_reference_time"):
        if n in da.dims:
            return n
    raise SystemExit(f"HATA: zaman boyutu bulunamadı. Boyutlar: {da.dims}")


def build(bbox: BBox, y0: int, y1: int, monthly_only: bool = False, allow_partial: bool = False) -> pd.DataFrame:
    # ---------------- aylık: tavg + prec --------------------------------
    mon = open_monthly(bbox)
    t2m = _var(mon, "t2m", "2m_temperature")
    tp = _var(mon, "tp", "total_precipitation")
    tdim = _time_dim(t2m)
    sel = t2m[tdim].dt.year
    t2m = t2m.where((sel >= y0) & (sel <= y1), drop=True)
    tp = tp.where((tp[tdim].dt.year >= y0) & (tp[tdim].dt.year <= y1), drop=True)

    # Aylık iklim normali: aynı takvim ayının yıllar boyunca ortalaması.
    tavg = (t2m.groupby(f"{tdim}.month").mean(tdim) - KELVIN)

    # DİKKAT: aylık üründe tp birimi m/GÜN'dür (ayın toplamı DEĞİL).
    # Aylık toplam mm = deger * 1000 * o ayin gun sayisi.
    days_in_month = xr.DataArray(
        [calendar.monthrange(2001, m)[1] for m in range(1, 13)],
        dims="month",
        coords={"month": list(range(1, 13))},
    )
    prec = tp.groupby(f"{tdim}.month").mean(tdim) * 1000.0 * days_in_month

    # ---------------- günlük minimum: tmin + absMin + don ----------------
    # `--monthly-only`: günlük dosyalar henüz inmemişken aylık yarıyı DOĞRULAMAK
    # için. Birim dönüşümü (m/gün -> mm/ay) ve ızgara anahtarı hataları saatlerce
    # indirme beklemeden burada yakalanır. Üretim çıktısı DEĞİLDİR: tmin ve
    # absMin boş kalır, dosya adına `MONTHLYONLY` eklenir.
    tmin_v = abs_v = frost_v = ls_v = fa_v = None
    if not monthly_only:
        day = open_daily(bbox, y0, y1, allow_partial=allow_partial)
        tmin_da = _var(day, "t2m", "2m_temperature") - KELVIN
        ddim = _time_dim(tmin_da)

        tmin = tmin_da.groupby(f"{ddim}.month").mean(ddim)
        abs_min = tmin_da.min(ddim)
        n_years = y1 - y0 + 1
        frost_days = (tmin_da < 0).sum(ddim) / n_years

        # Don tarihleri: yıl bazında son ilkbahar (doy<=200), ilk sonbahar (doy>200)
        doy = tmin_da[ddim].dt.dayofyear
        is_frost = tmin_da < 0
        spring = is_frost.where(doy <= 200, other=False)
        autumn = is_frost.where(doy > 200, other=False)
        last_spring = (doy * spring).groupby(f"{ddim}.year").max(ddim).mean("year")
        first_autumn = (
            (doy.where(autumn, other=999)).groupby(f"{ddim}.year").min(ddim).mean("year")
        )

        print("  günlük hesaplanıyor (dask -> bellek)...")
        tmin_v = tmin.compute().values
        abs_v = abs_min.compute().values
        frost_v = frost_days.compute().values
        ls_v = last_spring.compute().values
        fa_v = first_autumn.compute().values

    # ---------------- hesapla ve düzleştir ------------------------------
    tavg_v = tavg.compute().values if hasattr(tavg, "compute") else tavg.values
    prec_v = prec.compute().values if hasattr(prec, "compute") else prec.values

    lat_name = "latitude" if "latitude" in tavg.coords else "lat"
    lng_name = "longitude" if "longitude" in tavg.coords else "lon"
    lats = tavg[lat_name].values
    lngs = tavg[lng_name].values

    rows: list[dict] = []
    for i, la in enumerate(lats):
        for j, ln in enumerate(lngs):
            tv = tavg_v[:, i, j]
            pv = prec_v[:, i, j]
            if not np.isfinite(tv).all():
                continue  # deniz hücresi / veri yok
            tn = None if tmin_v is None else tmin_v[:, i, j]
            if tn is not None and not np.isfinite(tn).all():
                continue
            mean_temp = float(np.mean(tv))
            annual_rain = float(np.sum(pv))
            # En kurak 3 ARDIŞIK ay (yıl döngüsel).
            rolled = [float(np.sum(np.take(pv, range(m, m + 3), mode="wrap"))) for m in range(12)]
            rows.append(
                {
                    "grid_key": grid_key(float(la), float(ln)),
                    "lat": round(float(la), 4),
                    "lng": round(float(ln), 4),
                    "tavg": [round(float(x), 2) for x in tv],
                    "tmin": None if tn is None else [round(float(x), 2) for x in tn],
                    "prec": [round(float(x), 1) for x in pv],
                    "abs_min": None if abs_v is None else round(float(abs_v[i, j]), 1),
                    "mean_temp": round(mean_temp, 1),
                    "annual_rain": round(annual_rain),
                    "frost_days": None if frost_v is None else int(round(float(frost_v[i, j]))),
                    "last_spring_frost_doy": None if ls_v is None else int(round(float(ls_v[i, j]))),
                    "first_autumn_frost_doy": None if fa_v is None else int(round(float(fa_v[i, j]))),
                    "frost_free_days": None
                    if (fa_v is None or ls_v is None)
                    else int(round(float(fa_v[i, j]) - float(ls_v[i, j]))),
                    # De Martonne kuraklık indeksi = P / (T + 10). Yalnız aylık
                    # sıcaklık + yağıştan çıkar, EK İNDİRME İSTEMEZ.
                    # NOT: aridityClass alanını DOLDURMAZ (o ET0 tabanlıdır);
                    # ayrı alan olarak saklanır, UI kararı sonraya bırakıldı.
                    "de_martonne": round(annual_rain / (mean_temp + 10.0), 1)
                    if mean_temp > -10
                    else None,
                    "driest_quarter_mm": round(min(rolled)),
                    "period": f"{y0}-{y1}",
                    "source": "ERA5-Land",
                    "attribution": COPERNICUS_ATTRIBUTION,
                }
            )
    return pd.DataFrame(rows)


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--full", action="store_true")
    ap.add_argument(
        "--partial",
        action="store_true",
        help="günlük yıllar eksikken bilerek üret (çıktı PARTIAL olarak adlandırılır)",
    )
    ap.add_argument(
        "--monthly-only",
        action="store_true",
        help="günlük dosyalar inmeden aylık yarıyı DOĞRULA (üretim çıktısı değil)",
    )
    ap.add_argument(
        "--overlap",
        action="store_true",
        help=f"eşdeğerlik için {OVERLAP_START_YEAR}-{OVERLAP_END_YEAR} alt dönemi (ek indirme YOK)",
    )
    args = ap.parse_args()

    bbox = TURKEY_BBOX if args.full else PILOT_BBOX
    y0, y1 = (
        (OVERLAP_START_YEAR, OVERLAP_END_YEAR) if args.overlap else (NORMAL_START_YEAR, NORMAL_END_YEAR)
    )

    print(f"Kapsam: {bbox.name} · Dönem: {y0}-{y1}"
          + ("  [SADECE AYLIK — doğrulama]" if args.monthly_only else ""))
    df = build(bbox, y0, y1, monthly_only=args.monthly_only, allow_partial=args.partial)

    OUT_DIR.mkdir(parents=True, exist_ok=True)
    suffix = "_MONTHLYONLY" if args.monthly_only else ("_PARTIAL" if args.partial else "")
    out = OUT_DIR / f"site_grid_{bbox.name}_{y0}-{y1}{suffix}.csv"
    df.to_csv(out, index=False)

    print(f"\nSatır (kara hücresi): {len(df):,}")
    print(f"Dosya: {out}  ({out.stat().st_size / 1024 / 1024:.1f} MB)")
    if len(df):
        print(f"Örnek: {df.iloc[0]['grid_key']}  "
              f"ort {df.iloc[0]['mean_temp']}°C  yağış {df.iloc[0]['annual_rain']}mm  "
              f"absMin {df.iloc[0]['abs_min']}°C")
    return 0


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