"""Phase 2: one-shot, memory-bounded, idempotent, verified DuckDB-warehouse -> TimescaleDB migration.

The warehouse is a single DuckDB file (~5-10M `features` rows, hundreds of MB). A naive whole-table read into
RAM OOM'd a 2Gi node, so this migration STREAMS: it walks the table one symbol at a time and, within each
symbol, pulls rows through a server-side cursor in fixed `chunk_rows` batches (`fetchmany`), bulk-upserting
each batch into the destination and dropping it before the next. Peak RAM is therefore bounded by `chunk_rows`
(NOT by symbol width or total rows). The destination upsert is `INSERT ... ON CONFLICT DO UPDATE` (reuses the
TimescaleFeatureStore write path), so re-running is idempotent — same rows overwrite in place, never duplicate.

Verification (default on): the destination's `features` row count must equal DuckDB's, AND a per-feature
checksum — `(count, round(sum(value), 6))` grouped by feature — must reconcile on both sides (float-tolerant).
The returned report carries the counts, the per-feature checksum table, and the pass/fail verdict.

Live impact: NONE. This only COPIES into the Timescale tables; the `FXHNT_FEATURE_STORE` flag stays `duckdb`.
"""
from __future__ import annotations

from typing import Any

import duckdb

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore

# round-to-6-decimals tolerance for the per-feature value checksum (float repr differs across engines).
_CHECKSUM_DECIMALS = 6


def migrate_duckdb_to_timescale(
    duckdb_path: str,
    dest: TimescaleFeatureStore | str,
    *,
    chunk_rows: int = 200_000,
    verify: bool = True,
    progress: Any = None,
) -> dict[str, Any]:
    """Migrate `features` + `ticker_membership` from the DuckDB warehouse at `duckdb_path` into `dest`
    (a `TimescaleFeatureStore` or a DSN string the store is built from). Streams `features` in bounded
    chunks of at most `chunk_rows` rows so RAM stays bounded regardless of total size; upserts are
    ON CONFLICT (idempotent). When `verify`, reconciles row counts + per-feature checksums and reports it.

    `progress`, if given, is called with human-readable status lines (e.g. `typer.echo`)."""
    if chunk_rows < 1:
        raise ValueError("chunk_rows must be >= 1")

    own_dest = isinstance(dest, str)
    store = TimescaleFeatureStore(dest) if own_dest else dest

    def _log(msg: str) -> None:
        if progress is not None:
            progress(msg)

    # read-only: never mutate the source warehouse; safe to point at the live DuckDB file.
    con = duckdb.connect(duckdb_path, read_only=True)
    try:
        symbols = [r[0] for r in con.execute(
            "SELECT DISTINCT symbol FROM features ORDER BY symbol"
        ).fetchall()]
        _log(f"migrating {len(symbols)} symbol(s) from {duckdb_path} (chunk_rows={chunk_rows})")

        total_rows = 0
        n_chunks = 0
        max_chunk = 0
        for i, symbol in enumerate(symbols, 1):
            # Server-side cursor scoped to ONE symbol; fetchmany keeps only `chunk_rows` rows in RAM.
            cur = con.execute(
                "SELECT ts, feature, value FROM features WHERE symbol = ? ORDER BY ts, feature",
                [symbol],
            )
            sym_rows = 0
            while True:
                batch = cur.fetchmany(chunk_rows)
                if not batch:
                    break
                n_chunks += 1
                max_chunk = max(max_chunk, len(batch))
                _upsert_chunk(store, symbol, batch)
                sym_rows += len(batch)
                total_rows += len(batch)
                del batch  # free before pulling the next chunk
            _log(f"  [{i}/{len(symbols)}] {symbol}: {sym_rows} rows  (cumulative {total_rows})")

        members = con.execute(
            "SELECT symbol, exchange, start_date, end_date FROM ticker_membership ORDER BY symbol"
        ).fetchall()
        store.upsert_membership([(s, e, sd, ed) for s, e, sd, ed in members])
        _log(f"membership: {len(members)} row(s)")

        report: dict[str, Any] = {
            "features_rows_written": total_rows,
            "membership_rows": len(members),
            "symbols": len(symbols),
            "chunks": n_chunks,
            "max_chunk_rows": max_chunk,
            "chunk_rows": chunk_rows,
        }
        if verify:
            report.update(_verify(con, store))
            _log(f"verification: {'PASS' if report['verified'] else 'FAIL'} "
                 f"({report['features_count_duckdb']} duckdb / {report['features_count_timescale']} timescale)")
        else:
            report["verified"] = None
        return report
    finally:
        con.close()
        if own_dest:
            store.close()


def _upsert_chunk(store: TimescaleFeatureStore, symbol: str, batch: list[tuple]) -> None:
    """Bulk-upsert one bounded chunk of (ts, feature, value) rows for a symbol via the store's write path
    (ON CONFLICT DO UPDATE) — one transaction per chunk, idempotent, never holds more than the chunk."""
    rows = [(symbol, int(ts), str(feature), float(value)) for ts, feature, value in batch]
    store.upsert_feature_rows(rows)


def _verify(con: duckdb.DuckDBPyConnection, store: TimescaleFeatureStore) -> dict[str, Any]:
    """Reconcile row counts + per-feature (count, round(sum,6)) checksums between DuckDB and the destination."""
    duck_total = con.execute("SELECT count(*) FROM features").fetchone()[0]
    duck_per_feature = {
        str(feat): (int(cnt), round(float(ssum), _CHECKSUM_DECIMALS))
        for feat, cnt, ssum in con.execute(
            "SELECT feature, count(*), sum(value) FROM features GROUP BY feature ORDER BY feature"
        ).fetchall()
    }
    ts_total, ts_per_feature = store.feature_checksums(decimals=_CHECKSUM_DECIMALS)

    per_feature: dict[str, dict[str, Any]] = {}
    all_features = sorted(set(duck_per_feature) | set(ts_per_feature))
    ok = duck_total == ts_total
    for feat in all_features:
        d_cnt, d_sum = duck_per_feature.get(feat, (0, 0.0))
        t_cnt, t_sum = ts_per_feature.get(feat, (0, 0.0))
        match = d_cnt == t_cnt and abs(d_sum - t_sum) <= 10 ** (-_CHECKSUM_DECIMALS)
        ok = ok and match
        per_feature[feat] = {
            "count_duckdb": d_cnt, "count_timescale": t_cnt,
            "sum_duckdb": d_sum, "sum_timescale": t_sum, "match": match,
        }
    return {
        "verified": ok,
        "features_count_duckdb": duck_total,
        "features_count_timescale": ts_total,
        "per_feature": per_feature,
    }
