"""Yesterday orphan completion check for heartbeat monitoring.

TELEM-02: Per-symbol validation that yesterday's orphan bar closes
at exactly crossover_tid - 1 (the last trade of the previous day).

MON-01: Aion symbols get a rolling-window freshness check instead
of the crossover_tid orphan check (Aion has no midnight reset).
"""

from __future__ import annotations

import logging
import time
from typing import Any

from opendeviationbar.crossover import get_crossover_tid
from opendeviationbar.symbol_registry import get_symbol_entries

from heartbeat.config import AION_FRESHNESS_WINDOW_MIN, get_heartbeat_symbols

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


def collect_yesterday_completion(
    client: Any,  # clickhouse_connect Client
    results: dict[str, bool | str],
) -> None:
    """Check yesterday's orphan bar completion per streaming symbol.

    For each enabled Binance spot symbol (via get_heartbeat_symbols()):
    - Day-mode symbols: crossover_tid orphan check (existing behavior)
    - Aion symbols: rolling-window freshness check (last bar within N min)
    """
    from opendeviationbar.clickhouse.stathera_query import FULL_TABLE

    # Load symbol entries for per-symbol mode lookup (safe fallback)
    try:
        entries = get_symbol_entries()
    except (OSError, ImportError, ValueError, KeyError):
        logger.warning(
            "Symbol registry load failed — all symbols treated as day-mode",
            exc_info=True,
        )
        entries = {}

    checked_symbols: list[str] = []

    for symbol in get_heartbeat_symbols():
        checked_symbols.append(symbol)

        # Determine ouroboros mode for this symbol
        entry = entries.get(symbol)
        mode = getattr(getattr(entry, "ouroboros_mode", None), "value", "aion") if entry else "aion"

        if mode == "aion":
            _check_aion_freshness(client, symbol, results, FULL_TABLE)
        else:
            _check_day_completion(client, symbol, results, FULL_TABLE)

    # Aggregate health gate: True unless a real mismatch (False) is detected
    results["yesterday_completion"] = all(
        results.get(f"yesterday_{s}") is not False for s in checked_symbols
    )


def _check_aion_freshness(
    client: Any,
    symbol: str,
    results: dict[str, bool | str],
    full_table: str,
) -> None:
    """Rolling-window freshness check for Aion symbols.

    Three states:
    - aion:fresh — last committed bar within freshness window
    - aion:developing — bar not committed yet, but sidecar is actively
      receiving WS trades (healthy, threshold not yet breached)
    - aion:stale — no committed bar AND sidecar not streaming this symbol
    """
    try:
        row = client.query(  # type: ignore[union-attr]
            f"""
            SELECT max(close_time_us), max(last_agg_trade_id)
            FROM {full_table} FINAL
            WHERE symbol = '{symbol}'
              AND ouroboros_mode = 'aion'
              AND close_time_us > 0
            """
        )
    except Exception:
        logger.warning(
            "Aion freshness query failed for %s", symbol, exc_info=True
        )
        results[f"yesterday_{symbol}"] = "query_failed"
        return

    max_close_us = row.result_rows[0][0] if row.result_rows else None
    max_committed_tid = row.result_rows[0][1] if row.result_rows else None
    if max_close_us is None:
        results[f"yesterday_{symbol}"] = "no_bars"
        return

    now_us = int(time.time() * 1_000_000)
    stale_min = (now_us - max_close_us) / 60_000_000

    if stale_min <= AION_FRESHNESS_WINDOW_MIN:
        results[f"yesterday_{symbol}"] = True
        results[f"yesterday_{symbol}_detail"] = (
            f"aion:fresh ({stale_min:.0f}m ago)"
        )
        return

    # Bar is older than window — check if sidecar is actively streaming
    # (forming bar receiving trades but hasn't breached threshold yet)
    sidecar_active = False
    try:
        import json as _json
        import urllib.request

        resp = urllib.request.urlopen(
            "http://localhost:8081/status", timeout=3
        )
        status = _json.loads(resp.read())
        sym_status = (status.get("symbols") or status).get(symbol, {})
        ws_tid = sym_status.get("ws_first_tid")
        if ws_tid and max_committed_tid and ws_tid > max_committed_tid:
            sidecar_active = True
    except (OSError, ValueError, KeyError):
        logger.debug("Sidecar /status unreachable for %s — treating as stale", symbol)

    if sidecar_active:
        # Healthy: bar is developing, trades flowing, threshold not yet breached
        results[f"yesterday_{symbol}"] = True
        results[f"yesterday_{symbol}_detail"] = (
            f"aion:developing ({stale_min:.0f}m, threshold not breached)"
        )
    else:
        results[f"yesterday_{symbol}"] = False
        results[f"yesterday_{symbol}_detail"] = (
            f"aion:stale ({stale_min:.0f}m, window={AION_FRESHNESS_WINDOW_MIN}m)"
        )


def _check_day_completion(
    client: Any,
    symbol: str,
    results: dict[str, bool | str],
    full_table: str,
) -> None:
    """Day-mode orphan completion check (existing behavior).

    1. Get midnight crossover TID (first trade of today)
    2. Query yesterday's orphan bar from ClickHouse
    3. Compare: orphan.last_agg_trade_id should == crossover_tid - 1
    """
    # Step 1: Get crossover TID
    crossover_tid = get_crossover_tid(symbol)
    if crossover_tid is None:
        results[f"yesterday_{symbol}"] = "query_failed"
        return

    # Step 2: Query yesterday's LAST bar (the orphan — last bar before midnight)
    # No is_orphan column in CH; orphan = last bar of the day by trade ID
    try:
        row = client.query(  # type: ignore[union-attr]
            f"""
            SELECT max(last_agg_trade_id)
            FROM {full_table} FINAL
            WHERE symbol = '{symbol}'
              AND toDate(open_time_us / 1000000) = yesterday()
              AND last_agg_trade_id > 0
            """
        )
    except (OSError, RuntimeError, ConnectionError, ValueError):
        logger.warning(
            "yesterday orphan query failed for %s", symbol, exc_info=True
        )
        results[f"yesterday_{symbol}"] = "query_failed"
        return

    # Step 3: Compare
    orphan_tid = row.result_rows[0][0] if row.result_rows else None
    if orphan_tid is None:
        results[f"yesterday_{symbol}"] = "no_orphan"
    else:
        expected = crossover_tid - 1
        if orphan_tid == expected:
            results[f"yesterday_{symbol}"] = True
            results[f"yesterday_{symbol}_detail"] = "clean"
        else:
            results[f"yesterday_{symbol}"] = False
            results[f"yesterday_{symbol}_detail"] = (
                f"expected {expected}, got {orphan_tid}"
            )
