# RESUME-02: Parquet-first gap fill for Aion sidecar startup
"""Parquet pre-fill for Aion gap fill on sidecar restart.

Reads trades from local Parquet tick cache before falling back to REST API.
Processes trades through Rust OpenDeviationBarProcessor per threshold,
writes bars to ClickHouse, and returns updated seed TIDs so fill_from_rest
only covers the remaining uncached range.

Non-fatal: any failure returns empty dict (full REST fill as fallback).
"""

from __future__ import annotations

import logging
import time
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import polars as pl

if TYPE_CHECKING:
    from pathlib import Path

    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache
    from opendeviationbar.storage.parquet import TickStorage

logger = logging.getLogger("opendeviationbar.sidecar")

# Staleness threshold for Parquet freshness check
_STALENESS_THRESHOLD_S = 600  # 10 minutes


def _parquet_prefill_aion(
    ch_cache: OpenDeviationBarCache,
    aion_symbols: list[str],
    resume_tids: dict[str, int | None],
    thresholds: list[int],
) -> dict[str, int]:
    """Pre-fill Aion gap with Parquet ticks before REST API fallback.

    Reads cached trades from local Parquet tick cache for each Aion symbol,
    filters to only trades beyond the resume TID, processes through Rust
    processors per threshold, writes bars to ClickHouse, and returns
    {symbol: last_completed_bar_tid} for re-seeding the engine.

    Parameters
    ----------
    ch_cache
        Shared OpenDeviationBarCache instance (ALLOC-01).
    aion_symbols
        List of Aion-mode symbols to pre-fill.
    resume_tids
        Mapping of symbol -> resume trade ID (or None to skip).
    thresholds
        Threshold values in decimal basis points (e.g., [250, 500]).

    Returns
    -------
    dict[str, int]
        Mapping of {symbol: last_completed_bar_tid} for symbols that
        produced complete bars from Parquet data. Symbols with missing,
        stale, or empty Parquet are omitted (REST will cover them).
    """
    try:
        return _parquet_prefill_aion_inner(
            ch_cache, aion_symbols, resume_tids, thresholds
        )
    except Exception:
        logger.warning("parquet-prefill: failed (non-fatal)", exc_info=True)
        return {}


def _parquet_prefill_aion_inner(
    ch_cache: OpenDeviationBarCache,
    aion_symbols: list[str],
    resume_tids: dict[str, int | None],
    thresholds: list[int],
) -> dict[str, int]:
    """Inner implementation (unwrapped from try/except for testability)."""
    from opendeviationbar.storage.parquet import TickStorage

    storage = TickStorage()
    ticks_dir = storage.ticks_dir
    result: dict[str, int] = {}
    t0 = time.monotonic()
    total_bars = 0
    symbols_processed = 0

    for symbol in aion_symbols:
        resume_tid = resume_tids.get(symbol)
        if resume_tid is None:
            continue

        sym_result = _prefill_symbol(
            ch_cache, storage, ticks_dir, symbol, resume_tid, thresholds,
        )
        if sym_result is not None:
            seed_tid, sym_bars = sym_result
            result[symbol] = seed_tid
            total_bars += sym_bars
            symbols_processed += 1

    elapsed = time.monotonic() - t0
    logger.info(
        "parquet-prefill: %d/%d aion symbols processed, %d bars written in %.1fs",
        symbols_processed,
        len(aion_symbols),
        total_bars,
        elapsed,
    )

    return result


def _prefill_symbol(
    ch_cache: OpenDeviationBarCache,
    storage: TickStorage,
    ticks_dir: Path,
    symbol: str,
    resume_tid: int,
    thresholds: list[int],
) -> tuple[int, int] | None:
    """Process a single symbol's Parquet ticks. Returns (seed_tid, bar_count) or None."""
    from opendeviationbar.processors.core import OpenDeviationBarProcessor

    today_str = datetime.now(UTC).strftime("%Y-%m-%d")
    parquet_path = ticks_dir / symbol / f"{today_str}.parquet"

    if not parquet_path.exists():
        logger.info(
            "parquet-prefill: %s no Parquet file for %s, skipping",
            symbol,
            today_str,
        )
        return None

    # Freshness check: file mtime vs now
    age_s = time.time() - parquet_path.stat().st_mtime
    if age_s > _STALENESS_THRESHOLD_S:
        logger.info(
            "parquet-prefill: %s stale (%.0fs old > %ds), skipping",
            symbol,
            age_s,
            _STALENESS_THRESHOLD_S,
        )
        return None

    # Process Parquet ticks through Rust processors
    try:
        processors = {
            thr: OpenDeviationBarProcessor(thr, symbol=symbol)
            for thr in thresholds
        }
        sym_bars = 0

        from opendeviationbar import __version__

        for chunk in storage.read_ticks_streaming(symbol, chunk_size=500_000):
            if chunk.is_empty():
                continue

            # Filter: only trades beyond resume_tid (RESUME-02)
            filtered = (
                chunk.filter(pl.col("agg_trade_id") > resume_tid)
                if "agg_trade_id" in chunk.columns
                else chunk
            )
            del chunk

            if filtered.is_empty():
                del filtered
                continue

            # Zero-copy Polars -> Arrow
            arrow_chunk = filtered.to_arrow()
            del filtered

            for thr, processor in processors.items():
                bars_arrow = processor.process_trades_arrow_native(arrow_chunk)
                bars_pl = pl.from_arrow(bars_arrow)
                del bars_arrow

                if not bars_pl.is_empty():
                    bars_df: pl.DataFrame = bars_pl if isinstance(bars_pl, pl.DataFrame) else pl.DataFrame(bars_pl)
                    ch_cache.store_bars_batch(
                        symbol=symbol,
                        threshold_decimal_bps=thr,
                        bars=bars_df,
                        version=f"{__version__}+parquet-prefill",
                        overlap_strategy="off",
                        ouroboros_mode="aion",
                    )
                    sym_bars += len(bars_df)
                del bars_pl

            del arrow_chunk

        # Collect best TID: min across all thresholds (most conservative)
        # so fill_from_rest covers any threshold that's behind
        best_tid: int | None = None
        for processor in processors.values():
            tid = processor.last_completed_bar_tid()
            if tid is not None and (best_tid is None or tid < best_tid):
                best_tid = tid
        del processors

        if best_tid is not None:
            logger.info(
                "parquet-prefill: %s -- %d bars, seed_tid=%d (age=%.0fs)",
                symbol,
                sym_bars,
                best_tid,
                age_s,
            )
            return best_tid, sym_bars

    except (OSError, RuntimeError, ConnectionError):
        logger.warning(
            "parquet-prefill: %s processing failed (non-fatal, skipping)",
            symbol,
            exc_info=True,
        )

    return None
