"""Bar rectification safety net -- re-fetches and overwrites bars with trade count deficits."""

from __future__ import annotations

import logging
import os
from datetime import UTC, datetime

logger = logging.getLogger(__name__)


def rectify_recent_bars(
    symbol: str,
    threshold_decimal_bps: int,
    lookback_hours: float = 2.0,
    deficit_threshold: float = 0.05,
) -> int:
    """Re-fetch and overwrite bars with significant trade count deficit.

    Compares ``agg_record_count`` in ClickHouse against expected count
    derived from ``last_agg_trade_id - first_agg_trade_id + 1`` for each
    bar in the lookback window. Bars with > ``deficit_threshold`` (5%)
    trade deficit are re-fetched via REST ``fromId`` pagination and
    reprocessed through the canonical Rust processor.

    To achieve bit-exact parity with the streaming sidecar:
    - Fetches ~N lookback trades *before* each bar's ``first_agg_trade_id``
      to populate the processor's ``trade_history`` ring buffer, ensuring
      all 16 ``lookback_*`` inter-bar features are computed (not None).
    - Detects boundary divergence: if the rectified bar has a different
      ``close_time_us`` than the original (because a previously-missing
      trade triggers a breach earlier/later), deletes the ghost bar via
      ``ALTER TABLE DELETE`` before writing the corrected bar.

    ``ReplacingMergeTree(computed_at)`` ensures the newer version wins
    for bars with matching primary keys.

    Parameters
    ----------
    symbol : str
        Trading symbol (e.g., "BTCUSDT").
    threshold_decimal_bps : int
        Threshold in decimal basis points.
    lookback_hours : float
        How far back to check for deficient bars (default 2h).
    deficit_threshold : float
        Fraction of expected trades below which a bar is rectified
        (default 0.05 = 5% deficit).

    Returns
    -------
    int
        Number of bars rectified.
    """
    import polars as pl

    from opendeviationbar.clickhouse import OpenDeviationBarCache
    from opendeviationbar.orchestration.helpers import (
        _create_processor,
        _parse_microstructure_env_vars,
    )
    from opendeviationbar.symbol_registry import get_symbol_entries

    try:
        _entries = get_symbol_entries()
        mode = _entries[symbol].ouroboros_mode.value
    except Exception:
        mode = "aion"
    cutoff_ms = int((datetime.now(UTC).timestamp() - lookback_hours * 3600) * 1000)

    # Determine lookback trade count for inter-bar feature warm-up
    _, _, _ = _parse_microstructure_env_vars(True, None, None)
    lookback_count = int(
        os.environ.get("OPENDEVIATIONBAR_INTER_BAR_LOOKBACK_COUNT", "200")
    )

    # Step 1: Query recent bars with their trade ID range and record count
    with OpenDeviationBarCache() as cache:
        result = cache.client.query(
            "SELECT close_time_us, first_agg_trade_id, last_agg_trade_id, "
            "       agg_record_count "
            "FROM opendeviationbar_cache.open_deviation_bars FINAL "
            "WHERE symbol = {symbol:String} "
            "  AND threshold_decimal_bps = {threshold:UInt32} "
            "  AND ouroboros_mode = {mode:String} "
            "  AND close_time_us > {cutoff_ms:Int64} "
            "ORDER BY close_time_us",
            parameters={
                "symbol": symbol,
                "threshold": threshold_decimal_bps,
                "mode": mode,
                "cutoff_ms": cutoff_ms,
            },
        )

    if not result.result_rows:
        return 0

    # Step 2: Identify bars with trade deficit
    deficient_bars: list[tuple[int, int, int, int]] = []
    for row in result.result_rows:
        close_time_us, first_id, last_id, actual_count = (
            int(row[0]),
            int(row[1]),
            int(row[2]),
            int(row[3]),
        )
        expected_count = last_id - first_id + 1
        if expected_count <= 0:
            continue
        if actual_count < expected_count * (1 - deficit_threshold):
            deficient_bars.append((close_time_us, first_id, last_id, actual_count))

    if not deficient_bars:
        return 0

    logger.info(
        "%s @ %d dbps: %d/%d bars have trade deficit in last %.1fh",
        symbol,
        threshold_decimal_bps,
        len(deficient_bars),
        len(result.result_rows),
        lookback_hours,
    )

    # Step 3: Re-fetch trades with lookback warm-up and reprocess
    rectified = 0
    for close_time_us, first_id, last_id, old_count in deficient_bars:
        try:
            # Fetch lookback trades to warm up trade_history for inter-bar
            # features. These trades precede the bar and won't produce bars
            # themselves (the bar boundary falls within the bar trades).
            lookback_from_id = max(0, first_id - lookback_count)
            lookback_trades: list[dict] = []
            if lookback_from_id < first_id:
                lookback_trades = _fetch_trades_for_bar(
                    symbol, lookback_from_id, first_id - 1
                )

            # Fetch the bar's actual trades (full range)
            bar_trades = _fetch_trades_for_bar(symbol, first_id, last_id)
            if not bar_trades:
                continue

            # Concatenate: lookback trades warm up trade_history, then bar
            # trades produce the actual bars with correct inter-bar features
            all_trades = lookback_trades + bar_trades

            processor = _create_processor(
                threshold_decimal_bps,
                include_microstructure=True,
                symbol=symbol,
            )
            bars = processor.process_trades(all_trades)
            if not bars:
                continue

            # Filter: keep only bars whose trade range overlaps the original
            # bar's range (exclude bars formed purely from lookback trades)
            rectified_bars = [
                b
                for b in bars
                if b["last_agg_trade_id"] >= first_id
                and b["first_agg_trade_id"] <= last_id
            ]
            if not rectified_bars:
                continue

            # Detect boundary divergence: if no rectified bar has the same
            # close_time_us as the original, the missing trades shifted the
            # breach point. Delete the ghost bar before writing corrections.
            rectified_close_times = {b["close_time_us"] for b in rectified_bars}
            if close_time_us not in rectified_close_times:
                _delete_ghost_bar(
                    symbol,
                    threshold_decimal_bps,
                    mode,
                    close_time_us,
                )

            bars_df = pl.DataFrame(rectified_bars)
            from opendeviationbar import __version__

            with OpenDeviationBarCache() as cache:
                cache.store_bars_batch(
                    symbol=symbol,
                    threshold_decimal_bps=threshold_decimal_bps,
                    bars=bars_df,
                    version=f"{__version__}+rectify",
                    overlap_strategy="replace",
                )

            new_count = len(bar_trades)
            logger.info(
                "%s @ %d: rectified bar close_time_us=%d "
                "(agg_count %d → %d, bars_produced=%d)",
                symbol,
                threshold_decimal_bps,
                close_time_us,
                old_count,
                new_count,
                len(rectified_bars),
            )
            _emit_rectification_event(
                symbol,
                threshold_decimal_bps,
                close_time_us,
                old_count,
                new_count,
            )
            rectified += 1

        except (RuntimeError, OSError, ConnectionError) as e:
            logger.warning(
                "%s @ %d: rectification failed for bar close_time_us=%d: %s",
                symbol,
                threshold_decimal_bps,
                close_time_us,
                e,
            )

    return rectified


def _delete_ghost_bar(
    symbol: str,
    threshold_decimal_bps: int,
    ouroboros_mode: str,
    close_time_us: int,
) -> None:
    """Delete a ghost bar caused by boundary divergence during rectification.

    When rectification discovers that missing trades shift the breach point,
    the original bar's ``close_time_us`` differs from the corrected bar's.
    Since ``ReplacingMergeTree`` deduplicates by primary key (which includes
    ``close_time_us``), the original bar would persist as a ghost.

    This issues a partition-scoped ``DELETE IN PARTITION`` to remove the stale row.
    """
    from opendeviationbar.clickhouse import OpenDeviationBarCache

    try:
        with OpenDeviationBarCache() as cache:
            cache.client.command(
                "ALTER TABLE opendeviationbar_cache.open_deviation_bars "
                f"DELETE IN PARTITION ('{symbol}', {threshold_decimal_bps}) "
                "WHERE ouroboros_mode = {mode:String} "
                "  AND close_time_us = {close_time_us:Int64} "
                "SETTINGS mutations_sync = 1",
                parameters={
                    "mode": ouroboros_mode,
                    "close_time_us": close_time_us,
                },
            )
        logger.info(
            "%s @ %d: deleted ghost bar close_time_us=%d (boundary divergence)",
            symbol,
            threshold_decimal_bps,
            close_time_us,
        )
    except (RuntimeError, OSError, ConnectionError) as e:
        logger.warning(
            "%s @ %d: failed to delete ghost bar close_time_us=%d: %s",
            symbol,
            threshold_decimal_bps,
            close_time_us,
            e,
        )


def _fetch_trades_for_bar(
    symbol: str,
    first_agg_trade_id: int,
    last_agg_trade_id: int,
) -> list[dict]:
    """Fetch all trades for a bar's ID range via REST ``fromId`` pagination."""
    from opendeviationbar.backfill import _fetch_aggtrades_by_id_with_retry

    all_trades: list[dict] = []
    current_id = first_agg_trade_id

    while current_id <= last_agg_trade_id:
        limit = min(1000, last_agg_trade_id - current_id + 1)
        batch = _fetch_aggtrades_by_id_with_retry(symbol, current_id, int(limit))
        if not batch:
            break

        for trade in batch:
            trade_id = trade.get("agg_trade_id", trade.get("a", 0))
            if trade_id > last_agg_trade_id:
                return all_trades
            all_trades.append(trade)

        last_batch_id = batch[-1].get("agg_trade_id", batch[-1].get("a", 0))
        if last_batch_id <= current_id:
            break  # No progress
        current_id = last_batch_id + 1

    return all_trades


def _emit_rectification_event(
    symbol: str,
    threshold: int,
    close_time_us: int,
    old_agg_count: int,
    new_agg_count: int,
) -> None:
    """Emit NDJSON telemetry event for a bar rectification."""
    try:
        import json

        event = {
            "ts": datetime.now(UTC).isoformat(),
            "event": "bar_rectified",
            "symbol": symbol,
            "threshold": threshold,
            "close_time_us": close_time_us,
            "old_agg_count": old_agg_count,
            "new_agg_count": new_agg_count,
        }
        # Append to events log (same as other telemetry)
        from pathlib import Path

        log_dir = Path(
            os.environ.get("OPENDEVIATIONBAR_LOG_DIR", "logs")
        )  # SSoT-OK: env-configurable log dir
        log_dir.mkdir(parents=True, exist_ok=True)
        with (log_dir / "events.jsonl").open("a") as f:
            f.write(json.dumps(event) + "\n")
    except (OSError, ImportError):
        pass  # Non-fatal telemetry
