"""One-shot backfill of the parquet climate cache into the TimescaleDB tables.

Run ONCE during the cutover, after ``alembic upgrade head`` has created the
``climate_history`` / ``climate_recent`` / ``climate_sync`` tables. It loads every
cached cell's parquet record into Postgres so a freshly-cut-over server serves
history/recent from the database immediately instead of refetching decades of
archive (which the upstream rate limit would throttle hard).

Idempotent and resumable: a cell whose stored sync timestamp already matches its
parquet mtime is skipped, and history rows upsert (``ON CONFLICT``), so re-running
after an interruption only does the missing work.

Key detail: rows are read RAW (``pl.read_parquet`` + ``_normalize_read``), NOT via
``climate.load_cached_*`` — the loaders apply ``_derive_metrics``, which replaces
raw relative humidity with absolute humidity. The DB must store the raw record the
live write path persists, so absolute humidity / wet-bulb stay read-time
derivations. The per-cell ``synced_at`` is set to the parquet FILE MTIME (not now),
so ``recent_stamp`` — hence every derived-payload validity token — is unchanged
across the cutover.

Usage:
    THERMOGRAPH_DATABASE_URL=postgresql+psycopg://thermograph:PW@127.0.0.1:5432/thermograph \\
      python migrate_cache_to_pg.py            # reads data/cache/*.parquet
"""
import os
import sys

import polars as pl

from data import climate
from data import climate_store
from data import grid


def _is_schema_complete(df: pl.DataFrame) -> bool:
    """The same NEW_COLS guard climate._read_history_cache applies: a record missing
    any post-original column predates the wind/humidity features and is skipped so
    the server refetches it lazily (with the current schema) on first touch."""
    return all(c in df.columns for c in climate.NEW_COLS)


def migrate() -> int:
    if not climate_store.is_postgres():
        sys.exit("THERMOGRAPH_DATABASE_URL must be a postgresql URL (the migration target)")
    if not os.path.isdir(climate.CACHE_DIR):
        print("No parquet cache directory yet — nothing to migrate.")
        return 0

    hist = recent = current = skipped = 0
    for fname in sorted(os.listdir(climate.CACHE_DIR)):
        if not fname.endswith(".parquet"):
            continue
        path = os.path.join(climate.CACHE_DIR, fname)
        is_recent = fname.endswith("_rf.parquet")
        cell_id = fname[:-len("_rf.parquet")] if is_recent else fname[:-len(".parquet")]
        try:
            grid.from_id(cell_id)   # validate the id is a real cell (skip stray files)
        except ValueError:
            print(f"  {fname}: unrecognized cache filename — skipped")
            skipped += 1
            continue

        mtime = os.path.getmtime(path)
        synced = (climate_store.recent_synced_at(cell_id) if is_recent
                  else climate_store.history_synced_at(cell_id))
        if synced >= mtime:
            current += 1
            continue

        try:
            df = climate._normalize_read(pl.read_parquet(path))
        except Exception as e:  # noqa: BLE001 - a truncated/corrupt file is a skip, not a crash
            print(f"  {fname}: unreadable ({e}) — skipped")
            skipped += 1
            continue

        if is_recent:
            climate_store.write_recent(cell_id, df, mtime)
            recent += 1
            print(f"  {cell_id}: recent bundle ({df.height} rows)")
        else:
            if not _is_schema_complete(df):
                print(f"  {cell_id}: pre-schema history — skipped (refetches lazily)")
                skipped += 1
                continue
            climate_store.write_history(cell_id, df, mtime)
            hist += 1
            print(f"  {cell_id}: history ({df.height} rows)")

    print(f"{hist} history + {recent} recent loaded, {current} already current, "
          f"{skipped} skipped.")
    return 0


if __name__ == "__main__":
    sys.exit(migrate())
