#!/usr/bin/env python3
"""Tier-1 probe, FCI variant: does the finer MTG-FCI 3.8 um (IR_38) pixel see
the 2026-06-30 Gaetano fire where SEVIRI (verdict: ABSENT, 3x3 max 293 K) did not?

FCI FDHSI Sicily chunks (31-34) are cached per-cycle under fci_cache/<product>/.
For each fire-night cycle we read bt_mir(IR_38)/bt_tir(IR_105), sample the pixel
nearest each truth point, and run the live detect_subpixel_v1 at default and
citizen-relaxed thresholds. Same verdict logic as the SEVIRI probe.

Truth: p1 37.4602054,13.2264283  p2 37.456807,13.224735  ~19:20Z 30 Jun 2026.

Runs in any batch environment.
"""
from __future__ import annotations

import argparse
import glob
import json
import logging
import os
import re
from datetime import datetime, timezone
from pathlib import Path

import numpy as np

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
log = logging.getLogger("probe_gaetano_fci")

TRUTH = [("p1", 37.4602054, 13.2264283), ("p2", 37.456807, 13.224735)]
_OPE_RE = re.compile(r"_OPE_(\d{14})_")


def _dir_ts(dpath: str):
    m = _OPE_RE.search(os.path.basename(dpath.rstrip("/")))
    if not m:
        return None
    return datetime.strptime(m.group(1), "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)


def _nearest_idx(lat_grid, lon_grid, lat0, lon0):
    d2 = (lat_grid - lat0) ** 2 + ((lon_grid - lon0) * np.cos(np.radians(lat0))) ** 2
    return np.unravel_index(int(np.nanargmin(d2)), d2.shape)


def _win3x3(arr, r, c):
    r0, r1 = max(0, r - 1), min(arr.shape[0], r + 2)
    c0, c1 = max(0, c - 1), min(arr.shape[1], c + 2)
    return arr[r0:r1, c0:c1]


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(prog="probe_gaetano_fci")
    ap.add_argument("--fci-cache", required=True)
    ap.add_argument("--start", default="2026-06-30T19:00")
    ap.add_argument("--end", default="2026-07-01T06:00")
    ap.add_argument("--out", required=True)
    args = ap.parse_args(argv)
    start = datetime.fromisoformat(args.start).replace(tzinfo=timezone.utc)
    end = datetime.fromisoformat(args.end).replace(tzinfo=timezone.utc)

    from src.data_sources.fci_l1c import FciL1cClient
    from src.detectors import subpixel_v1 as sp

    client = FciL1cClient()
    baseline = None

    dirs = sorted(glob.glob(os.path.join(args.fci_cache, "*_OPE_*")))
    cyc = [(d, _dir_ts(d)) for d in dirs if os.path.isdir(d)]
    cyc = [(d, t) for (d, t) in cyc if t is not None and start <= t <= end]
    log.info("FCI probe: %d cycles in window", len(cyc))

    series = []
    for dpath, ts in cyc:
        # Full-named Sicily chunk files (satpy fci_l1c_nc reader needs the W_XX name).
        chunks = sorted(glob.glob(os.path.join(dpath, "W_XX-*CHK-BODY*_00[0-9][0-9].nc")))
        if not chunks:
            chunks = sorted(glob.glob(os.path.join(dpath, "*BODY*_00[0-9][0-9].nc")))
        if not chunks:
            log.warning("no chunk files in %s", os.path.basename(dpath))
            continue
        try:
            fr = client._read_and_crop([Path(c) for c in chunks], os.path.basename(dpath))
        except Exception as exc:  # noqa: BLE001
            log.warning("read failed %s: %s", os.path.basename(dpath), exc)
            continue
        if baseline is None:
            baseline = sp._load_baseline(shape=fr.bt_mir.shape)

        det_default = sp.detect_subpixel_v1(
            fr.bt_mir, fr.bt_tir, fr.lat, fr.lon,
            source_stream="fci_l1c", timestamp=fr.scan_start or ts, baseline=baseline)
        det_relaxed = sp.detect_subpixel_v1(
            fr.bt_mir, fr.bt_tir, fr.lat, fr.lon,
            source_stream="fci_l1c", timestamp=fr.scan_start or ts,
            mir_threshold_k=290.0, lightning_primed_threshold_k=288.0,
            baseline_z_threshold=2.0, baseline=baseline)

        rec = {"ts": (fr.scan_start or ts).isoformat(), "points": {}}
        for name, la, lo in TRUTH:
            r, c = _nearest_idx(fr.lat, fr.lon, la, lo)
            bt_mir = float(fr.bt_mir[r, c]); bt_tir = float(fr.bt_tir[r, c])

            def _hit(dets):
                for d in dets:
                    if abs(getattr(d, "lat", 1e9) - la) < 0.03 and \
                       abs(getattr(d, "lng", getattr(d, "lon", 1e9)) - lo) < 0.03:
                        return True
                return False

            rec["points"][name] = {
                "bt_mir_k": round(bt_mir, 3),
                "bt_tir_k": round(bt_tir, 3),
                "delta_k": round(bt_mir - bt_tir, 3),
                "bt_mir_3x3_max_k": round(float(np.nanmax(_win3x3(fr.bt_mir, r, c))), 3),
                "subpixel_default_fired": _hit(det_default),
                "subpixel_relaxed_fired": _hit(det_relaxed),
            }
        series.append(rec)
        log.info("%s  p1 bt_mir=%.2f 3x3max=%.2f delta=%.2f", rec["ts"],
                 rec["points"]["p1"]["bt_mir_k"],
                 rec["points"]["p1"]["bt_mir_3x3_max_k"],
                 rec["points"]["p1"]["delta_k"])

    verdict = {}
    for name, _, _ in TRUTH:
        mir_max = max((s["points"][name]["bt_mir_3x3_max_k"] for s in series), default=None)
        dmax = max((s["points"][name]["delta_k"] for s in series), default=None)
        any_relaxed = any(s["points"][name]["subpixel_relaxed_fired"] for s in series)
        any_default = any(s["points"][name]["subpixel_default_fired"] for s in series)
        present = bool(any_relaxed or any_default or (mir_max is not None and mir_max >= 300.0))
        verdict[name] = {
            "max_bt_mir_3x3_k": mir_max, "max_delta_k": dmax,
            "relaxed_fired_any_frame": any_relaxed,
            "default_fired_any_frame": any_default,
            "signal_present": present,
            "call": ("PRESENT in FCI (finer pixel catches it — build FCI prior/floor)"
                     if present else
                     "ABSENT in FCI too (geostationary thermal cannot; DNB + S2 SWIR mandatory)"),
        }

    out = {"sensor": "fci_l1c", "truth_points": TRUTH, "window": [args.start, args.end],
           "n_frames": len(series), "verdict": verdict, "series": series}
    Path(args.out).write_text(json.dumps(out, indent=2, default=str))
    log.info("FCI VERDICT: %s", json.dumps(verdict, indent=2, default=str))
    log.info("wrote %s", args.out)
    return 0


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