"""Stathera integrity checks (gaps + overlaps) for heartbeat monitoring.

TELEM-01: Unified per-(symbol, threshold) result -- no intra-day vs midnight-boundary classification.
TELEM-04: LOUD Telegram alert when new gaps appear in today's bars.
TELEM-05: Kintsugi queue position per anomalous pair (#297).
"""

from __future__ import annotations

import logging
import os

from heartbeat.config import KNOWN_GAP_FINGERPRINTS
from heartbeat.state import load_stathera_counts, save_stathera_counts
from heartbeat.telegram import send_telegram

logger = logging.getLogger("health-heartbeat")


def collect_stathera(
    client: object,  # clickhouse_connect Client
    results: dict[str, bool | str],
) -> None:
    """Run Stathera audit: detect BOTH gaps and overlaps in aggTrade IDs.

    Unified per-symbol results (TELEM-01) and today-gap LOUD alert (TELEM-04).
    """
    from opendeviationbar.clickhouse.stathera_query import (
        FULL_TABLE,
        VALID_TID_FILTER,
        cross_pair_filter,
        cross_pair_window,
        enabled_symbols_sql,
    )

    # Check for pending async mutations
    pending_mutations_row = client.query(
        "SELECT countIf(is_done = 0) FROM system.mutations "
        "WHERE database = 'opendeviationbar_cache'"
    )
    pending_mutations = (
        pending_mutations_row.result_rows[0][0]
        if pending_mutations_row.result_rows
        else 0
    )
    results["stathera_pending_mutations"] = str(pending_mutations)

    # ── Unified per-symbol query (TELEM-01 + TELEM-05) ───────────────────
    # max(open_time_us) is needed for kintsugi queue position sorting (#297).
    per_symbol_row = client.query(
        f"""
        SELECT symbol, threshold_decimal_bps,
               countIf(tid_delta < 0) AS overlaps,
               countIf(tid_delta > 0) AS gaps,
               max(open_time_us) AS latest_open_us
        FROM (
            SELECT symbol, threshold_decimal_bps,
                   open_time_us,
                   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
            {cross_pair_filter()}
            {cross_pair_window()}
        )
        WHERE rn > 1 AND tid_delta != 0
        GROUP BY symbol, threshold_decimal_bps
        """
    )

    # Build per-symbol dict and compute totals
    per_symbol: dict[str, dict] = {}
    overlap_count = 0
    gap_count = 0
    for row in per_symbol_row.result_rows:
        sym, thr, overlaps, gaps, latest_open_us = row
        key = f"{sym}@{thr}"
        per_symbol[key] = {
            "gaps": gaps,
            "overlaps": overlaps,
            "latest_open_us": int(latest_open_us) if latest_open_us else 0,
        }
        overlap_count += overlaps
        gap_count += gaps

    results["stathera_per_symbol"] = per_symbol

    # ── Exchange gap classification from kintsugi_log ────────────────────
    # Cross-reference kintsugi_log for exchange_gap entries to split total
    # gaps into pipeline (actionable) vs exchange (permanent, unhealable).
    exchange_gap_lookup: dict[str, int] = {}  # pair_key -> exchange_gap_count
    try:
        exchange_gap_result = client.query(
            """
            SELECT symbol, threshold_decimal_bps, count() AS exchange_gaps
            FROM opendeviationbar_cache.kintsugi_log
            WHERE result = 'exchange_gap'
            GROUP BY symbol, threshold_decimal_bps
            """
        )
        for row in exchange_gap_result.result_rows:
            sym, thr, eg_count = row
            key = f"{sym}@{thr}"
            exchange_gap_lookup[key] = eg_count
    except Exception:
        logger.debug("kintsugi_log exchange_gap query failed, treating all gaps as pipeline", exc_info=True)

    # Enrich per_symbol with pipeline/exchange split and compute aggregates
    total_exchange_gaps = 0
    for pair_key, counts in per_symbol.items():
        pair_total_gaps = counts.get("gaps", 0)
        # Cap exchange gaps at the pair's total gaps
        pair_exchange = min(exchange_gap_lookup.get(pair_key, 0), pair_total_gaps)
        pair_pipeline = pair_total_gaps - pair_exchange
        counts["exchange_gaps"] = pair_exchange
        counts["pipeline_gaps"] = pair_pipeline
        total_exchange_gaps += pair_exchange

    pipeline_gap_count = gap_count - total_exchange_gaps
    exchange_gap_count = total_exchange_gaps

    # ── TELEM-05: Kintsugi queue position per anomalous pair (#297) ───────
    # Cross-reference each anomalous pair against kintsugi's sort order so the
    # Telegram message shows "queue #N/M" next to each per-symbol line.
    #
    # Sort key mirrors _shard_sort_key() in kintsugi.py:
    #   (-kintsugi_priority, is_genesis=0 for Stathera shards, -gap_end_us)
    # We use latest_open_us as a proxy for gap_end_us (close enough for display).
    #
    # A pair is kintsugi-eligible if it has anomalies NOT in today
    # (kintsugi today-guard #280 blocks today's bars).
    try:
        _compute_kintsugi_queue_positions(
            client, results, per_symbol, FULL_TABLE, VALID_TID_FILTER
        )
    except Exception:
        logger.debug("kintsugi queue position computation failed", exc_info=True)

    # Delta tracking: compare with previous heartbeat to detect growth
    prev = load_stathera_counts()
    prev_overlaps = prev.get("overlap_count", overlap_count)
    prev_gaps = prev.get("gap_count", gap_count)
    prev_pipeline_gaps = prev.get("pipeline_gap_count", prev_gaps)
    prev_exchange_gaps = prev.get("exchange_gap_count", 0)
    overlap_delta = overlap_count - prev_overlaps
    gap_delta = gap_count - prev_gaps
    pipeline_gap_delta = pipeline_gap_count - prev_pipeline_gaps
    exchange_gap_delta = exchange_gap_count - prev_exchange_gaps
    save_stathera_counts(
        overlap_count,
        gap_count,
        pipeline_gap_count=pipeline_gap_count,
        exchange_gap_count=exchange_gap_count,
    )

    # Health logic — only pipeline gaps trigger CRITICAL, exchange gaps are permanent
    kintsugi_healing = (
        results.get("kintsugi") is True and results.get("kintsugi_stuck") is not False
    )
    mutations_in_flight = pending_mutations > 0
    # Use pipeline_gap_delta for growth detection — exchange gaps growing is
    # expected during retroactive classification and should not trigger alerts
    anomalies_growing = overlap_delta > 0 or pipeline_gap_delta > 0
    has_anomalies = overlap_count > 0 or pipeline_gap_count > 0

    if not has_anomalies or (
        mutations_in_flight and overlap_count > 0 and pipeline_gap_count == 0
    ):
        results["stathera_overlaps"] = True
        results["stathera_gaps"] = True
    elif anomalies_growing and not kintsugi_healing:
        results["stathera_overlaps"] = overlap_count == 0
        results["stathera_gaps"] = pipeline_gap_count == 0
    else:
        results["stathera_overlaps"] = overlap_count == 0 or kintsugi_healing
        results["stathera_gaps"] = pipeline_gap_count == 0 or kintsugi_healing

    results["stathera_overlap_count"] = str(overlap_count)
    results["stathera_gap_count"] = str(gap_count)
    results["stathera_pipeline_gap_count"] = str(pipeline_gap_count)
    results["stathera_exchange_gap_count"] = str(exchange_gap_count)
    results["stathera_overlap_delta"] = str(overlap_delta)
    results["stathera_gap_delta"] = str(gap_delta)
    results["stathera_pipeline_gap_delta"] = str(pipeline_gap_delta)
    results["stathera_exchange_gap_delta"] = str(exchange_gap_delta)

    # Today-only anomaly counts (#295 P0-3): writer-ownership-aware.
    # Kintsugi CANNOT heal today (#280 today-guard), so today's anomalies
    # must NEVER be suppressed by kintsugi_healing.
    try:
        sym_filter = (
            enabled_symbols_sql()
        )  # already returns "AND symbol IN (...)" or ""

        today_result = client.query(
            f"""
            SELECT countIf(td > 0) AS today_gaps, countIf(td < 0) AS today_overlaps
            FROM (
                SELECT first_agg_trade_id - lagInFrame(last_agg_trade_id, 1) OVER w - 1 AS td,
                       row_number() OVER w AS rn
                FROM {FULL_TABLE} FINAL
                WHERE toDate(open_time_us / 1000000) = today()
                  AND {VALID_TID_FILTER} {sym_filter}
                WINDOW w AS (PARTITION BY symbol, threshold_decimal_bps, ouroboros_mode
                             ORDER BY first_agg_trade_id, close_time_us)
            ) WHERE rn > 1 AND td != 0
            """
        )
        today_gaps_count, today_overlaps_count = (
            today_result.result_rows[0] if today_result.result_rows else (0, 0)
        )
    except Exception:
        logger.warning("Today-anomaly count query failed", exc_info=True)
        today_gaps_count, today_overlaps_count = 0, 0

    # These are NEVER suppressed by kintsugi_healing — kintsugi can't touch today
    results["stathera_today_gaps"] = today_gaps_count == 0
    results["stathera_today_overlaps"] = today_overlaps_count == 0
    results["stathera_today_gap_count"] = str(today_gaps_count)
    results["stathera_today_overlap_count"] = str(today_overlaps_count)

    # Top 5 unique pairs per anomaly type
    if overlap_count > 0:
        top = client.query(
            f"""
            SELECT concat(symbol, '@', toString(threshold_decimal_bps)) AS pair,
                   count() AS cnt
            FROM (
                SELECT symbol, threshold_decimal_bps,
                       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
                {cross_pair_filter()}
                {cross_pair_window()}
            )
            WHERE tid_delta < 0 AND rn > 1
            GROUP BY symbol, threshold_decimal_bps
            ORDER BY cnt DESC
            LIMIT 5
            """
        )
        pairs = [f"{r[0]}({r[1]})" for r in top.result_rows]
        if pairs:
            results["stathera_overlap_pairs"] = ", ".join(pairs)
    if gap_count > 0:
        top = client.query(
            f"""
            SELECT concat(symbol, '@', toString(threshold_decimal_bps)) AS pair,
                   count() AS cnt
            FROM (
                SELECT symbol, threshold_decimal_bps,
                       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
                {cross_pair_filter()}
                {cross_pair_window()}
            )
            WHERE tid_delta > 0 AND rn > 1
            GROUP BY symbol, threshold_decimal_bps
            ORDER BY cnt DESC
            LIMIT 5
            """
        )
        pairs = [f"{r[0]}({r[1]})" for r in top.result_rows]
        if pairs:
            results["stathera_gap_pairs"] = ", ".join(pairs)

    # Fingerprint active gaps against known root causes
    if gap_count > 0:
        try:
            gap_detail = client.query(
                f"""
                SELECT symbol, threshold_decimal_bps, prev_last_tid,
                       first_agg_trade_id, tid_delta
                FROM (
                    SELECT symbol, threshold_decimal_bps,
                           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
                    {cross_pair_filter()}
                    {cross_pair_window()}
                )
                WHERE tid_delta > 0 AND rn > 1
                ORDER BY tid_delta DESC
                LIMIT 20
                """
            )
            fingerprints: list[dict] = []
            all_known = True
            for gap_row in gap_detail.result_rows:
                sym, thr, prev_tid, first_tid, tdelta = gap_row
                key = (sym, int(thr), int(first_tid), int(prev_tid))
                known = KNOWN_GAP_FINGERPRINTS.get(key)
                if known:
                    fingerprints.append(
                        {
                            "symbol": sym,
                            "threshold": int(thr),
                            "first_agg_trade_id": int(first_tid),
                            "prev_last_tid": int(prev_tid),
                            "tid_delta": int(tdelta),
                            **known,
                        }
                    )
                else:
                    all_known = False
            results["stathera_gap_fingerprints"] = fingerprints
            results["stathera_all_gaps_known"] = str(
                all_known and len(fingerprints) > 0
            )
        except Exception:
            logger.warning("Gap fingerprint query failed", exc_info=True)

    # ── TELEM-04: Today's gap LOUD alert ─────────────────────────────────
    _check_today_gaps(
        client,
        results,
        gap_delta=gap_delta,
        kintsugi_healing=kintsugi_healing,
        full_table=FULL_TABLE,
        valid_tid_filter=VALID_TID_FILTER,
        enabled_symbols_sql_fn=enabled_symbols_sql,
    )


def _compute_kintsugi_queue_positions(
    client: object,
    results: dict,
    per_symbol: dict[str, dict],
    full_table: str,
    valid_tid_filter: str,
) -> None:
    """Compute kintsugi queue position for each anomalous pair (TELEM-05, #297).

    Anomalies that are exclusively in today's bars are kintsugi-ineligible
    (#280 today-guard).  All other pairs are eligible and sorted by
    kintsugi's own _shard_sort_key: (-priority, -latest_open_us).

    Writes ``stathera_kintsugi_queue`` into results:
        {pair_key: {"queue_pos": N, "total": M, "today_only": bool}}
    """
    if not per_symbol:
        return

    from opendeviationbar.symbol_registry import get_kintsugi_priority

    # Query per-pair today-only anomaly counts to detect kintsugi-ineligible pairs.
    # A pair is "today-only" if ALL its anomalies fall within today (UTC).
    sym_list = list(per_symbol.keys())  # e.g. ["BTCUSDT@250", "ETHUSDT@500"]
    if not sym_list:
        return

    # Build an IN filter from anomalous pairs for the today-only check
    pair_filter_parts = []
    for pair_key in sym_list:
        parts = pair_key.rsplit("@", 1)
        if len(parts) == 2:
            sym, thr = parts[0], parts[1]
            pair_filter_parts.append(
                f"(symbol = '{sym}' AND threshold_decimal_bps = {thr})"
            )
    if not pair_filter_parts:
        return
    pair_in_filter = " OR ".join(pair_filter_parts)

    # Count anomalies per pair for bars NOT in today — these are kintsugi-eligible.
    today_only_result = client.query(
        f"""
        SELECT symbol, threshold_decimal_bps,
               countIf(tid_delta != 0 AND rn > 1) AS all_anomalies,
               countIf(tid_delta != 0 AND rn > 1
                       AND toDate(open_time_us / 1000000, 'UTC') < today()) AS past_anomalies
        FROM (
            SELECT symbol, threshold_decimal_bps, open_time_us,
                   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
            WHERE ({pair_in_filter}) AND {valid_tid_filter}
            WINDOW w AS (PARTITION BY symbol, threshold_decimal_bps, ouroboros_mode
                         ORDER BY first_agg_trade_id, close_time_us)
        )
        GROUP BY symbol, threshold_decimal_bps
        """
    )

    # Build today_only lookup: pair_key -> bool
    today_only: dict[str, bool] = {}
    for row in today_only_result.result_rows:
        sym, thr, _all_anom, past_anom = row
        key = f"{sym}@{thr}"
        today_only[key] = (
            past_anom == 0
        )  # True = all anomalies in today = kintsugi can't help

    # Sort eligible pairs by kintsugi priority: highest priority first, then most recent first.
    # Mirrors kintsugi._shard_sort_key: (-priority, is_genesis=0, -gap_end_us)
    eligible: list[tuple[str, int, int]] = []  # (pair_key, -priority, -latest_open_us)
    for pair_key, counts in per_symbol.items():
        is_today_only = today_only.get(pair_key, False)
        if not is_today_only:
            parts = pair_key.rsplit("@", 1)
            sym = parts[0] if len(parts) == 2 else pair_key
            priority = get_kintsugi_priority(sym)
            latest_open_us = counts.get("latest_open_us") or 0
            eligible.append((pair_key, -priority, -latest_open_us))

    eligible.sort(
        key=lambda x: (x[1], x[2])
    )  # ascending: lowest -priority first = highest priority first

    total = len(eligible)
    queue: dict[str, dict] = {}

    for i, (pair_key, _neg_priority, _neg_latest_us) in enumerate(eligible, start=1):
        queue[pair_key] = {"queue_pos": i, "total": total, "today_only": False}

    # Add today-only entries (not in queue, kintsugi cannot fix)
    for pair_key in per_symbol:
        if pair_key not in queue:
            queue[pair_key] = {"queue_pos": None, "total": total, "today_only": True}

    results["stathera_kintsugi_queue"] = queue


def _check_today_gaps(
    client: object,
    results: dict[str, bool | str],
    *,
    gap_delta: int,
    kintsugi_healing: bool,
    full_table: str,
    valid_tid_filter: str,
    enabled_symbols_sql_fn: object,
) -> None:
    """Check for new gaps in today's bars and fire LOUD alert if found."""
    try:
        sym_filter = enabled_symbols_sql_fn()
        today_gaps = client.query(
            f"""
            SELECT symbol, threshold_decimal_bps,
                   prev_last_tid, first_agg_trade_id, tid_delta
            FROM (
                SELECT symbol, threshold_decimal_bps,
                       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
                WHERE toDate(open_time_us / 1000000) = today()
                  AND {valid_tid_filter} {sym_filter}
                WINDOW w AS (PARTITION BY symbol, threshold_decimal_bps, ouroboros_mode
                             ORDER BY first_agg_trade_id, close_time_us)
            )
            WHERE tid_delta > 0 AND rn > 1
            """
        )

        if not today_gaps.result_rows:
            return

        if gap_delta <= 0:
            return

        # Build LOUD alert message
        lines = ["<b>NEW STATHERA GAP IN TODAY'S BARS</b>", ""]
        for row in today_gaps.result_rows:
            sym, thr, prev_tid, first_tid, tdelta = row
            lines.append(
                f"{sym}@{thr}: tid_delta={tdelta}\n"
                f"  prev_last_tid={prev_tid} -> first_agg_trade_id={first_tid}"
            )

        msg = "\n".join(lines)
        token = os.environ.get("OPENDEVIATIONBAR_TELEGRAM_TOKEN", "")
        chat_id = os.environ.get("OPENDEVIATIONBAR_TELEGRAM_CHAT_ID", "90417581")
        if token:
            send_telegram(token, chat_id, msg, silent=False)
            results["stathera_today_gap_alert"] = True
            logger.warning(
                "LOUD alert sent for %d today-gap(s)", len(today_gaps.result_rows)
            )

    except Exception:
        logger.warning("Today-gap check failed", exc_info=True)
