from __future__ import annotations

import logging
import time
from collections import defaultdict
from datetime import UTC
from pathlib import Path

import polars as pl

from .arrow_flush import _sync_flush_rest_bars_arrow
from .clickhouse_write import _write_batch_with_retry
from .http_server import _junction_state

logger = logging.getLogger(__name__)


def _get_midnight_crossover_tid(symbol: str) -> int | None:
    """Backward-compatible wrapper. Delegates to shared crossover module.

    All crossover lookup logic now lives in opendeviationbar.crossover.
    This wrapper exists so sidecar/__init__.py's import still resolves.
    """
    from opendeviationbar.crossover import get_crossover_tid

    return get_crossover_tid(symbol)  # date=None means today


def _verify_yesterday_orphan(symbol: str, crossover_tid: int) -> bool:
    """Verify yesterday's orphan bar closes at exactly crossover_tid - 1.

    PREFLIGHT-02: Queries ClickHouse for yesterday's orphan bar and checks
    that orphan.last_agg_trade_id == crossover_tid - 1 (perfect Stathera continuity).

    Returns True if verification passes, False if mismatch or missing.
    Never raises -- logs WARNING on any issue and returns False.
    """
    try:
        from opendeviationbar.clickhouse.cache import OpenDeviationBarCache

        with OpenDeviationBarCache() as cache:
            orphan_tid = cache.get_yesterday_orphan_tid(symbol)

        if orphan_tid is None:
            logger.info(
                "no orphan bar for %s yesterday -- fresh deploy or gap, deferring to kintsugi",
                symbol,
            )
            return False

        if orphan_tid == crossover_tid - 1:
            logger.info(
                "midnight continuity verified for %s: orphan_last_tid=%d, crossover_tid=%d",
                symbol,
                orphan_tid,
                crossover_tid,
            )
            return True

        logger.warning(
            "midnight continuity MISMATCH for %s: orphan_last_tid=%d, expected=%d, crossover_tid=%d"
            " -- deferring to kintsugi",
            symbol,
            orphan_tid,
            crossover_tid - 1,
            crossover_tid,
        )
        return False  # noqa: TRY300
    except Exception:
        logger.warning(
            "midnight orphan verification failed for %s", symbol, exc_info=True
        )
        return False


def _check_yesterday_integrity(
    symbols: list[str],  # noqa: ARG001
    thresholds: list[int],  # noqa: ARG001
    preflight_verified: dict[str, int],
) -> dict[str, str]:
    """Check yesterday's data integrity without repairing (STARTUP-01).

    Fast check (~3s for 16 symbols) that replaces the blocking
    ``_repair_yesterday_if_needed()`` in the sidecar startup path.

    For each symbol with a verified crossover TID:
    1. Orphan check: yesterday's orphan bar last_agg_trade_id == crossover_tid - 1
    2. Stathera audit: zero gaps AND zero overlaps for yesterday (all thresholds)

    Returns
    -------
    dict[str, str]
        Mapping of {symbol: reason} for broken symbols. Empty dict means all clean.
        Reasons: "orphan_misaligned", "orphan_missing", "stathera_gap",
        "stathera_overlap", "ch_unavailable".
    """
    if not preflight_verified:
        logger.info("yesterday integrity: no preflight data, skipping check")
        return {}

    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache
    from opendeviationbar.clickhouse.stathera_query import (
        FULL_TABLE,
        STATHERA_CORE_COLUMNS,
        VALID_TID_FILTER,
    )
    from opendeviationbar.symbol_registry import get_symbol_entries

    flagged: dict[str, str] = {}

    try:
        try:
            _sym_entries = get_symbol_entries()
        except Exception:
            _sym_entries = {}

        with OpenDeviationBarCache() as cache:
            for symbol, crossover_tid in sorted(preflight_verified.items()):
                # Per-symbol ouroboros mode from registry
                try:
                    _entry = _sym_entries[symbol]
                    ouroboros_mode = _entry.ouroboros_mode.value
                except Exception:
                    ouroboros_mode = "aion"

                # Aion symbols have no orphan bars at midnight -- skip orphan check
                if ouroboros_mode == "aion":
                    logger.info(
                        "yesterday integrity: %s is aion mode, skipping orphan check",
                        symbol,
                    )
                    continue

                # --- Orphan check ---
                orphan_tid = cache.get_yesterday_orphan_tid(symbol)
                expected_tid = crossover_tid - 1

                if orphan_tid is None:
                    flagged[symbol] = "orphan_missing"
                    logger.warning(
                        "yesterday integrity: %s orphan missing (expected last_tid=%d)",
                        symbol,
                        expected_tid,
                    )
                    continue
                if orphan_tid != expected_tid:
                    flagged[symbol] = "orphan_misaligned"
                    logger.warning(
                        "yesterday integrity: %s orphan misaligned "
                        "(orphan_last_tid=%d, expected=%d)",
                        symbol,
                        orphan_tid,
                        expected_tid,
                    )
                    continue

                # --- Stathera audit for yesterday (all thresholds in one query) ---
                try:
                    result = cache.client.query(
                        f"""
                        SELECT threshold_decimal_bps, tid_delta
                        FROM (
                            SELECT threshold_decimal_bps, {STATHERA_CORE_COLUMNS}
                            FROM {FULL_TABLE} FINAL
                            WHERE symbol = {{symbol:String}}
                              AND ouroboros_mode = {{ouroboros:String}}
                              AND toDate(open_time_us / 1000000, 'UTC') = yesterday()
                              AND {VALID_TID_FILTER}
                            WINDOW w AS (
                                PARTITION BY threshold_decimal_bps
                                ORDER BY first_agg_trade_id, close_time_us
                            )
                        )
                        WHERE rn > 1 AND tid_delta != 0
                        LIMIT 1
                        """,
                        parameters={
                            "symbol": symbol,
                            "ouroboros": ouroboros_mode,
                        },
                    )
                    if result.result_rows:
                        tid_delta = result.result_rows[0][1]
                        if tid_delta > 0:
                            flagged[symbol] = "stathera_gap"
                            logger.warning(
                                "yesterday integrity: %s Stathera gap (tid_delta=%d)",
                                symbol,
                                tid_delta,
                            )
                        else:
                            flagged[symbol] = "stathera_overlap"
                            logger.warning(
                                "yesterday integrity: %s Stathera overlap (tid_delta=%d)",
                                symbol,
                                tid_delta,
                            )
                except Exception:
                    flagged[symbol] = "ch_unavailable"
                    logger.warning(
                        "yesterday integrity: Stathera query failed for %s",
                        symbol,
                        exc_info=True,
                    )

    except (ConnectionError, OSError, RuntimeError) as exc:
        # CH completely unavailable — flag all symbols
        logger.warning(
            "yesterday integrity: CH unavailable (%s), flagging all %d symbols",
            exc,
            len(preflight_verified),
        )
        return dict.fromkeys(preflight_verified, "ch_unavailable")

    # Summary log
    if flagged:
        logger.warning(
            "yesterday integrity: %d/%d symbols broken: %s",
            len(flagged),
            len(preflight_verified),
            ", ".join(f"{s}={r}" for s, r in sorted(flagged.items())),
        )
    else:
        logger.info(
            "yesterday integrity: all %d symbols clean", len(preflight_verified)
        )

    return flagged


def _repair_yesterday_if_needed(
    symbols: list[str],
    thresholds: list[int],
    preflight_verified: dict[str, int],
    valid_pairs: set[tuple[str, int]] | None,
) -> None:
    """Verify yesterday is clean. If orphan bar missing/misaligned, repair.

    Chronological invariant: yesterday MUST be correct before today's
    fill_from_rest, so today's first bar connects seamlessly to yesterday's
    last bar. Without this, restarts after midnight leave a gap at the
    day boundary that persists until kintsugi repairs tomorrow — too late.

    For each symbol with a verified crossover TID, checks if yesterday's
    last bar ends at crossover_tid - 1. If not, invokes kintsugi's
    day-recompute for yesterday (synchronous, blocks until complete).
    """
    import datetime as dt

    if not preflight_verified:
        logger.info("yesterday repair: no preflight data, skipping")
        return

    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache

    yesterday = dt.datetime.now(dt.UTC).date() - dt.timedelta(days=1)
    yesterday_start_us = int(
        dt.datetime.combine(yesterday, dt.time.min, tzinfo=dt.UTC).timestamp()
        * 1_000_000
    )
    yesterday_end_us = yesterday_start_us + 86_400_000_000

    # Collect symbols that need yesterday repair
    symbols_to_repair: list[str] = []
    with OpenDeviationBarCache() as cache:
        for symbol, crossover_tid in sorted(preflight_verified.items()):
            orphan_tid = cache.get_yesterday_orphan_tid(symbol)
            expected_tid = crossover_tid - 1

            if orphan_tid == expected_tid:
                continue  # yesterday is clean for this symbol

            if orphan_tid is None:
                logger.warning(
                    "yesterday repair: %s missing orphan bar (expected last_tid=%d)",
                    symbol,
                    expected_tid,
                )
            else:
                logger.warning(
                    "yesterday repair: %s orphan misaligned "
                    "(orphan_last_tid=%d, expected=%d, gap=%d trades)",
                    symbol,
                    orphan_tid,
                    expected_tid,
                    expected_tid - orphan_tid,
                )
            symbols_to_repair.append(symbol)

    if not symbols_to_repair:
        logger.info("yesterday repair: all %d symbols clean", len(preflight_verified))
        return

    logger.info(
        "yesterday repair: %d/%d symbols need repair: %s",
        len(symbols_to_repair),
        len(preflight_verified),
        symbols_to_repair,
    )

    # Repair each symbol's yesterday via kintsugi day-recompute
    from opendeviationbar.clickhouse.write_lock import cache_write_lock
    from opendeviationbar.kintsugi import Shard, _repair_under_lock
    from opendeviationbar.symbol_registry import get_symbol_entries

    try:
        _repair_sym_entries = get_symbol_entries()
    except Exception:
        _repair_sym_entries = {}

    registered_pairs = valid_pairs or {
        (sym, thr) for sym in symbols for thr in thresholds
    }
    repaired = 0
    for symbol in symbols_to_repair:
        # Per-symbol ouroboros mode from registry
        try:
            _re = _repair_sym_entries[symbol]
            _repair_mode = _re.ouroboros_mode.value
        except Exception:
            _repair_mode = "aion"

        # Repair all thresholds for this symbol
        symbol_thresholds = sorted(
            thr for sym, thr in registered_pairs if sym == symbol
        )
        for threshold in symbol_thresholds:
            shard = Shard(
                symbol=symbol,
                threshold_decimal_bps=threshold,
                gap_start_us=yesterday_start_us,
                gap_end_us=yesterday_end_us,
                gap_hours=24.0,
                anchor_last_tid=0,
                resume_first_tid=0,
            )
            try:
                t0 = time.monotonic()
                with cache_write_lock(symbol, threshold, timeout=300):
                    written = _repair_under_lock(
                        shard=shard,
                        day_start_us=yesterday_start_us,
                        day_end_us=yesterday_end_us,
                        repair_date=yesterday,
                        ouroboros_mode=_repair_mode,
                        include_microstructure=True,
                        verbose=False,
                    )
                elapsed_s = time.monotonic() - t0
                logger.info(
                    "yesterday repair: %s@%d — %d bars in %.1fs",
                    symbol,
                    threshold,
                    written,
                    elapsed_s,
                )
                repaired += 1
            except Exception:
                logger.warning(
                    "yesterday repair: %s@%d failed (non-fatal, kintsugi will retry)",
                    symbol,
                    threshold,
                    exc_info=True,
                )

    logger.info(
        "yesterday repair: completed %d pairs for %d symbols",
        repaired,
        len(symbols_to_repair),
    )


def _heal_day_boundary_gaps(
    engine: object,
    valid_pairs: set[tuple[str, int]],
) -> None:
    """Deterministic day-boundary gap heal on startup (#286).

    Checks ClickHouse for Stathera gaps in today's bars. For each gap found,
    submits fill_gap() to the live engine. Covers ALL edge cases: manual bar
    deletions, stale checkpoints, multi-restart scenarios, and any situation
    where today's data has trade-ID discontinuities.

    Uses the live engine's fill_gap() — rate-limited parallel REST fetch,
    checkpoint-restored processor state, correct bar boundaries.
    """
    try:
        from opendeviationbar.clickhouse.cache import OpenDeviationBarCache
        from opendeviationbar.clickhouse.stathera_query import (
            FULL_TABLE,
            per_pair_filter,
            per_pair_window,
        )
        from opendeviationbar.symbol_registry import (
            filter_pairs_by_registry,
            get_symbol_entries,
        )

        try:
            _heal_sym_entries = get_symbol_entries()
        except Exception:
            _heal_sym_entries = {}

        cache = OpenDeviationBarCache()
        pairs = filter_pairs_by_registry(valid_pairs)
        total_healed = 0

        for symbol, threshold in sorted(pairs):
            # Per-symbol ouroboros mode from registry
            try:
                _he = _heal_sym_entries[symbol]
                ouroboros_mode = _he.ouroboros_mode.value
            except Exception:
                ouroboros_mode = "aion"

            # Aion symbols have no day boundary concept -- skip
            if ouroboros_mode == "aion":
                logger.debug(
                    "day-boundary heal: %s is aion mode, skipping",
                    symbol,
                )
                continue

            try:
                # Query for gaps touching today (including yesterday→today boundary)
                result = cache.client.query(
                    f"""
                    SELECT prev_last_tid, first_agg_trade_id, tid_delta
                    FROM (
                        SELECT
                            lagInFrame(last_agg_trade_id, 1) OVER w AS prev_last_tid,
                            first_agg_trade_id,
                            first_agg_trade_id
                              - lagInFrame(last_agg_trade_id, 1) OVER w - 1
                              AS tid_delta,
                            row_number() OVER w AS rn
                        FROM {FULL_TABLE} FINAL
                        {per_pair_filter()}
                        AND toDate(open_time_us / 1000000) = today()
                        {per_pair_window()}
                    )
                    WHERE tid_delta > 0 AND rn > 1
                    ORDER BY first_agg_trade_id
                    """,
                    parameters={
                        "symbol": symbol,
                        "threshold": int(threshold),
                        "ouroboros": ouroboros_mode,
                    },
                )
                rows = result.result_rows if result.result_rows else []
                for row in rows:
                    anchor_tid, resume_tid, delta = (
                        int(row[0]),
                        int(row[1]),
                        int(row[2]),
                    )
                    if delta > 0:
                        # Gap: fill missing trades via live engine
                        logger.info(
                            "day-boundary heal: %s@%d gap delta=%d — submitting fill_gap",
                            symbol,
                            threshold,
                            delta,
                        )
                        try:
                            engine.fill_gap(symbol, anchor_tid + 1, resume_tid)  # type: ignore[attr-defined]
                            total_healed += 1
                        except Exception:
                            logger.warning(
                                "day-boundary heal: fill_gap failed for %s@%d (non-fatal)",
                                symbol,
                                threshold,
                                exc_info=True,
                            )
            except Exception:
                logger.debug(
                    "day-boundary heal: query failed for %s@%d", symbol, threshold
                )

        if total_healed > 0:
            logger.info(
                "day-boundary heal: submitted %d fill_gap commands", total_healed
            )
        else:
            logger.info("day-boundary heal: no gaps found — continuity clean")

    except Exception:
        logger.warning("day-boundary heal failed (non-fatal)", exc_info=True)


def _sync_flush_rest_bars(
    engine: object,
    valid_pairs: set[tuple[str, int]] | None,
    *,
    symbol_modes: dict[str, str] | None = None,
    cache: object | None = None,
) -> int:
    """Dispatch to Arrow or dict drain path. See _sync_flush_rest_bars_arrow/dict."""
    if hasattr(engine, "drain_bars_arrow"):
        return _sync_flush_rest_bars_arrow(
            engine, valid_pairs, symbol_modes=symbol_modes, cache=cache
        )
    return _sync_flush_rest_bars_dict(
        engine, valid_pairs, symbol_modes=symbol_modes, cache=cache
    )


def _sync_flush_rest_bars_dict(  # noqa: PLR0915
    engine: object,
    valid_pairs: set[tuple[str, int]] | None,
    *,
    symbol_modes: dict[str, str] | None = None,
    cache: object | None = None,
) -> int:
    """Dict-based sync_flush: legacy fallback for OdbEngine or older builds (#296)."""
    day_us = 86_400_000_000

    if hasattr(engine, "drain_bars"):
        all_bars: list[dict] = engine.drain_bars()  # type: ignore[attr-defined]
    else:
        # Fallback for OdbEngine or older builds without drain_bars
        all_bars = []
        while True:
            bar = engine.next_bar(timeout_ms=500)  # type: ignore[attr-defined]
            if bar is None:
                break
            all_bars.append(bar)

    raw_count = len(all_bars)
    batches: dict[tuple[str, int], list[dict]] = defaultdict(list)
    drained = 0
    cross_midnight_count = 0
    _seen_rejected: set[str] = set()

    if valid_pairs is not None:
        vp_sample = sorted(valid_pairs)[:5]
        vp_types = {
            (type(s).__name__, type(t).__name__) for s, t in list(valid_pairs)[:3]
        }
        logger.info(
            "sync_flush: valid_pairs=%d, sample=%s, types=%s",
            len(valid_pairs),
            vp_sample,
            vp_types,
        )

    for bar in all_bars:
        symbol = bar.pop("_symbol", "UNKNOWN")
        threshold = bar.pop("_threshold", 0)

        # Skip pairs not in valid_pairs
        if valid_pairs is not None and (symbol, threshold) not in valid_pairs:
            _pair_key = f"{symbol}@{threshold}"
            if _pair_key not in _seen_rejected:
                _seen_rejected.add(_pair_key)
                logger.info(
                    "sync_flush: REJECTED %s (sym_type=%s, thr_type=%s, thr_repr=%r)",
                    _pair_key,
                    type(symbol).__name__,
                    type(threshold).__name__,
                    threshold,
                )
            continue

        # L1: cross-midnight filter (#264) — only for Day-mode symbols.
        # Aion symbols intentionally span midnight without reset.
        open_us = bar.get("open_time_us", 0)
        close_us = bar.get("close_time_us", 0)
        sym_mode = (symbol_modes or {}).get(symbol, "aion")
        if (
            sym_mode == "day"
            and open_us > 0
            and close_us > 0
            and (open_us // day_us) != (close_us // day_us)
        ):
            cross_midnight_count += 1
            logger.warning(
                "sync_flush: cross-midnight bar discarded: %s@%d open=%d close=%d",
                symbol,
                threshold,
                open_us,
                close_us,
            )
            continue

        batches[(symbol, threshold)].append(bar)
        drained += 1

    # MEM: Release the raw drain list — bars are now in per-pair batches.
    # This frees ~800MB of dict references for a 200K-bar drain.
    del all_bars

    filtered_invalid_pairs = raw_count - drained - cross_midnight_count
    logger.info(
        "sync_flush: drain diagnostics: raw=%d, valid=%d, cross_midnight=%d, invalid_pair=%d",
        raw_count,
        drained,
        cross_midnight_count,
        filtered_invalid_pairs,
    )

    if drained == 0:
        logger.info(
            "sync_flush: ring buffer empty — nothing to flush (raw drain: %d bars)",
            raw_count,
        )
        return 0

    # --- Stathera continuity check within each (symbol, threshold) batch ---
    # Diagnostic only: gaps and non-monotonic ordering are logged but never
    # block the CH write. fill_from_rest should produce gapless bars, but
    # ring buffer overflow or edge cases could introduce discontinuities.
    for (sym, thr), batch_bars in batches.items():
        if len(batch_bars) < 2:  # noqa: PLR2004
            continue
        # LAZY-02: In-place sort avoids copying the entire batch list.
        batch_bars.sort(key=lambda b: b.get("first_agg_trade_id", 0))
        for i in range(1, len(batch_bars)):
            prev_last = batch_bars[i - 1].get("last_agg_trade_id", 0)
            curr_first = batch_bars[i].get("first_agg_trade_id", 0)
            if curr_first <= prev_last:
                logger.warning(
                    "sync_flush: stathera non-monotonic in REST batch %s@%d: "
                    "bar[%d].last_tid=%d >= bar[%d].first_tid=%d",
                    sym,
                    thr,
                    i - 1,
                    prev_last,
                    i,
                    curr_first,
                )
            elif curr_first != prev_last + 1:
                logger.warning(
                    "sync_flush: stathera gap in REST batch %s@%d: "
                    "bar[%d].last_tid=%d -> bar[%d].first_tid=%d (delta=%d)",
                    sym,
                    thr,
                    i - 1,
                    prev_last,
                    i,
                    curr_first,
                    curr_first - prev_last - 1,
                )

    from opendeviationbar import __version__
    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache

    source_version = f"{__version__}+sidecar"
    written = 0

    # ALLOC-01/03: Use shared cache and pre-computed symbol_modes
    _modes = symbol_modes or {}
    _cache = cache
    if _cache is None:
        _cache = OpenDeviationBarCache()

    for (symbol, threshold), bars in batches.items():
        _mode = _modes.get(symbol, "aion")
        try:
            bars_df = pl.DataFrame(bars)
            inserted = _cache.store_bars_batch(
                symbol=symbol,
                threshold_decimal_bps=threshold,
                bars=bars_df,
                version=source_version,
                overlap_strategy="off",
                ouroboros_mode=_mode,
                _skip_lock=False,
            )
            written += inserted
        except Exception:
            logger.info(
                "sync_flush: store_bars_batch failed for %s@%d, "
                "falling back to _write_batch_with_retry (non-fatal)",
                symbol,
                threshold,
                exc_info=True,
            )
            if _write_batch_with_retry(symbol, threshold, bars, cache=_cache, ouroboros_mode=_mode):
                written += len(bars)

    # Populate junction telemetry for /status endpoint
    for (symbol, _threshold), bars in batches.items():
        if symbol not in _junction_state:
            _junction_state[symbol] = {}
        last_tid = max(
            (b.get("last_agg_trade_id", 0) for b in bars),
            default=0,
        )
        if last_tid > 0:
            _junction_state[symbol]["rest_last_tid"] = last_tid
        min_tid = min(
            (b.get("first_agg_trade_id", 0) for b in bars),
            default=0,
        )
        if min_tid > 0:
            _junction_state[symbol]["rest_first_tid"] = min(
                _junction_state[symbol].get("rest_first_tid", min_tid),
                min_tid,
            )

    # Diagnostic: log which pairs were written and which had zero bars
    pairs_with_bars = sorted(
        ((s, t, len(bars)) for (s, t), bars in batches.items()),
        key=lambda x: (-x[2], x[0], x[1]),
    )
    logger.info(
        "sync_flush: %d/%d REST bars written to CH before start() (%d pairs)",
        written,
        drained,
        len(batches),
    )
    _max_pairs_to_log = 50
    if len(pairs_with_bars) <= _max_pairs_to_log:
        for sym, thr, cnt in pairs_with_bars:
            logger.debug("sync_flush: wrote %s@%d = %d bars", sym, thr, cnt)

    return written


def _run_midnight_preflight(
    engine: object,
    symbols: list[str],
    thresholds: list[int],
) -> dict[str, int]:
    """Run midnight preflight verification for all streaming symbols.

    PREFLIGHT-03: For each symbol, queries the crossover trade ID (first trade
    after UTC midnight) and verifies yesterday's orphan bar. If verified,
    overrides the engine's trade-ID seed for ALL thresholds of that symbol
    with crossover_tid - 1, so fill_from_rest starts from the exact right point.

    Returns dict mapping symbol -> crossover_tid for symbols that passed preflight.
    Symbols that fail preflight keep their existing CH-seeded values (graceful fallback).
    """
    # Filter out forex/Exness symbols — they have no Binance aggTrade IDs,
    # so crossover TID lookup is meaningless and always fails.
    from opendeviationbar.symbol_registry import get_symbol_entries

    entries = get_symbol_entries()
    binance_symbols = [
        s for s in symbols if s in entries and entries[s].exchange == "binance"
    ]
    skipped = len(symbols) - len(binance_symbols)
    if skipped > 0:
        logger.info(
            "midnight preflight: skipping %d non-Binance symbols (forex/exness)",
            skipped,
        )

    verified: dict[str, int] = {}
    for symbol in binance_symbols:
        crossover_tid = _get_midnight_crossover_tid(symbol)
        if crossover_tid is None:
            logger.warning(
                "preflight skipped for %s: could not determine crossover trade ID",
                symbol,
            )
            continue

        # Verification is log-only — does not gate seed override.
        _verify_yesterday_orphan(symbol, crossover_tid)

        # Override CH seed with verified crossover TID for all thresholds.
        for threshold in thresholds:
            engine.seed_trade_id(symbol, threshold, crossover_tid - 1)

        verified[symbol] = crossover_tid

    logger.info(
        "midnight preflight: verified %d/%d symbols, overrode seeds for %d pairs",
        len(verified),
        len(symbols),
        len(verified) * len(thresholds),
    )
    return verified


def _read_parquet_max_tids(
    symbols: list[str],
    *,
    ticks_dir: Path | None = None,
) -> dict[str, int]:
    """Read max(agg_trade_id) from today's Parquet tick cache per symbol.

    Returns dict of {symbol: max_trade_id} for symbols with fresh Parquet data.
    Symbols with missing, stale (>10 min), or corrupt Parquet files are omitted.

    Parameters
    ----------
    symbols : list[str]
        Bare symbol names (e.g., ["BTCUSDT", "ETHUSDT"]).
    ticks_dir : Path | None
        Override ticks directory (for testing). If None, uses TickStorage default.
    """
    from datetime import datetime

    staleness_threshold_s = 600  # 10 minutes

    if ticks_dir is None:
        from opendeviationbar.storage.parquet import TickStorage

        ticks_dir = TickStorage().ticks_dir

    today_str = datetime.now(UTC).strftime("%Y-%m-%d")
    result: dict[str, int] = {}

    for symbol in symbols:
        parquet_path = ticks_dir / symbol / f"{today_str}.parquet"
        if not parquet_path.exists():
            continue

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

        # Read max agg_trade_id from Parquet (lazy scan, minimal I/O)
        try:
            max_tid = (
                pl.scan_parquet(parquet_path)
                .select(pl.col("agg_trade_id").max())
                .collect()
                .item()
            )
            if max_tid is not None and max_tid > 0:
                result[symbol] = int(max_tid)
                logger.info(
                    "parquet-first: %s max_tid=%d (age=%.0fs)",
                    symbol,
                    max_tid,
                    age_s,
                )
        except Exception:
            logger.debug("parquet-first: failed to read %s", symbol, exc_info=True)

    return result


def _process_parquet_ticks(  # noqa: PLR0915
    symbols: list[str],
    thresholds: list[int],
    *,
    ticks_dir: Path | None = None,
) -> dict[str, int]:
    """Process today's Parquet ticks into bars and return seed TIDs.

    SEED-FIX-01: Reads today's Parquet ticks via TickStorage, processes them
    through Rust OpenDeviationBarProcessor per (symbol, threshold), writes
    bars to ClickHouse, and returns {symbol: last_completed_bar_tid} for
    seeding fill_from_rest.

    SEED-FIX-02: Returns last_completed_bar_tid (NOT max agg_trade_id) so
    fill_from_rest correctly re-processes the forming bar's tail trades.

    Parameters
    ----------
    symbols : list[str]
        Bare symbol names (e.g., ["BTCUSDT", "ETHUSDT"]).
    thresholds : list[int]
        Threshold values in decimal basis points (e.g., [250, 500]).
    ticks_dir : Path | None
        Override ticks directory (for testing). If None, uses TickStorage default.

    Returns
    -------
    dict[str, int]
        Mapping of {symbol: last_completed_bar_tid} for symbols that produced
        complete bars. Symbols with stale/missing/empty Parquet or processing
        errors are omitted.
    """
    from datetime import datetime

    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache
    from opendeviationbar.processors.core import OpenDeviationBarProcessor
    from opendeviationbar.storage.parquet import TickStorage

    staleness_threshold_s = 600  # 10 minutes

    storage = TickStorage()
    if ticks_dir is None:
        ticks_dir = storage.ticks_dir

    today_str = datetime.now(UTC).strftime("%Y-%m-%d")
    result: dict[str, int] = {}

    # v7.0: Pre-load symbol entries for per-symbol ouroboros_mode on CH write
    from opendeviationbar.symbol_registry import get_symbol_entries as _gsym

    try:
        _all_sym_entries = _gsym()
    except Exception:
        _all_sym_entries = {}

    for symbol in symbols:
        parquet_path = ticks_dir / symbol / f"{today_str}.parquet"
        if not parquet_path.exists():
            continue

        _sym_e = _all_sym_entries.get(symbol)
        _symbol_mode = _sym_e.ouroboros_mode.value if _sym_e else "aion"

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

        try:
            # Memory-efficient: create processors first, stream chunks through them.
            # Each processor maintains state across chunks (forming bar continuity).
            # This avoids loading the entire day's Parquet into memory at once.
            processors = {
                thr: OpenDeviationBarProcessor(thr, symbol=symbol) for thr in thresholds
            }
            best_tid: int | None = None
            total_bars = 0
            chunk_count = 0

            with OpenDeviationBarCache() as cache:
                from opendeviationbar import __version__

                for chunk in storage.read_ticks_streaming(symbol, chunk_size=500_000):
                    if chunk.is_empty():
                        continue
                    chunk_count += 1
                    # Zero-copy Polars → Arrow (shared memory, no copy)
                    arrow_chunk = chunk.to_arrow()
                    del chunk  # release Polars DataFrame immediately

                    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  # release Arrow result

                        if not bars_pl.is_empty():
                            cache.store_bars_batch(
                                symbol=symbol,
                                threshold_decimal_bps=thr,
                                bars=bars_pl,
                                version=f"{__version__}+parquet",
                                overlap_strategy="off",
                                ouroboros_mode=_symbol_mode,
                            )
                            total_bars += len(bars_pl)
                        del bars_pl  # release Polars bars

                    del arrow_chunk  # release Arrow source after all thresholds

            # Collect best TID after all chunks processed
            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  # release all processor state

            if chunk_count == 0:
                continue  # empty Parquet file

            if best_tid is not None:
                result[symbol] = best_tid
                logger.info(
                    "parquet-process: %s -- %d bars across %d thresholds "
                    "(%d chunks), seed_tid=%d (age=%.0fs)",
                    symbol,
                    total_bars,
                    len(thresholds),
                    chunk_count,
                    best_tid,
                    age_s,
                )
        except Exception:
            logger.warning(
                "parquet-process: %s failed (non-fatal, skipping)",
                symbol,
                exc_info=True,
            )

    return result
