"""Telegram message formatter for heartbeat monitoring."""

from __future__ import annotations

import html
import socket
import time

from heartbeat.checks.resources import RESOURCE_WINDOW_MINUTES
from heartbeat.config import HEARTBEAT_PRIORITY_SYMBOLS


def format_message(results: dict[str, bool | str], healthy: bool) -> str:
    """Format health check results as a sectioned Telegram message."""
    host = socket.gethostname()
    now = time.strftime("%H:%M:%S UTC", time.gmtime())

    def _icon(key: str) -> str:
        v = results.get(key)
        if v is True:
            return "\u2705"
        if v is False:
            return "\u274c"
        return "\u2139\ufe0f"

    # Per-check remediation hints for UNHEALTHY state
    _remediation = {
        "clickhouse": "Check: systemctl status clickhouse-server",
        "sidecar": "Check: journalctl --user -u opendeviationbar-sidecar -n 20",
        "kintsugi": "Check: journalctl --user -u opendeviationbar-kintsugi -n 20",
        "kintsugi_telemetry": "Kintsugi may be stuck or crashed — check journal",
        "kintsugi_stuck": "Repair running too long — check REST rate limits or CH writes",
        "kintsugi_failures": "Multiple repairs failing — check Binance Vision data + CH health",
        "stathera_gaps": "Bar-boundary gaps detected (different processor instances produced non-adjacent bars) — kintsugi day-recompute will resolve",
        "stathera_overlaps": "Duplicate bars from different sidecar instances awaiting ReplacingMergeTree merge — not data loss",
        "cross_midnight": "Cross-midnight bars detected and kintsugi is not healing — check kintsugi service",
        "disk": "Disk space critical — check du -sh /var/lib/clickhouse/",
        "circuit_breaker": "CH writes blocked — check: curl -s 'http://localhost:8123/?query=SELECT+1'",
        "version_drift": "Version mismatch! Run: mise run deploy:bigblack",
        "seeder_ok": "Seeder timer stale or symbol failed — journalctl --user -u opendeviationbar-seeder -n 30",
        "yesterday_completion": "Yesterday's orphan bar doesn't match crossover TID — kintsugi will repair tomorrow",
    }

    if healthy:
        lines = [f"\U0001f49a <b>Heartbeat OK</b> \u2014 {host} @ {now}"]
    else:
        lines = [f"\U0001f534 <b>UNHEALTHY</b> \u2014 {host} @ {now}"]
        # Collect failed checks with remediation hints
        failed_checks = [k for k in _remediation if results.get(k) is False]
        if failed_checks:
            lines.append("")
            lines.append("<b>Likely cause</b>")
            for k in failed_checks:
                lines.append(f"  \u2192 {k}: {_remediation[k]}")

    # -- Services (with uptime + memory) --
    lines.append("")
    lines.append("<b>Services</b>")
    lines.append(f"  {_icon('clickhouse')} clickhouse")
    for svc in ("sidecar", "kintsugi"):
        uptime = results.get(f"{svc}_uptime", "")
        mem = results.get(f"{svc}_mem", "")
        suffix_parts = []
        if uptime:
            suffix_parts.append(f"{uptime} \u2191")
        if mem:
            suffix_parts.append(f"{mem} MB")
        suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
        lines.append(f"  {_icon(svc)} {svc}{suffix}")

    # Seeder (timer-driven oneshot — no persistent PID, shown in Services)
    if "seeder_ok" in results:
        status = results.get("seeder_status", "")
        if status == "running":
            running_min = results.get("seeder_running_min", "?")
            cur_sym = results.get("seeder_current_symbol", "")
            months_done = results.get("seeder_months_done", "")
            days_done = results.get("seeder_days_done", "")
            journal_month = results.get("seeder_journal_month", "")
            journal_line = results.get("seeder_journal_line", "")
            progress_parts = []
            if cur_sym:
                progress_parts.append(cur_sym)
                if months_done:
                    progress_parts.append(f"{months_done}mo")
                if days_done:
                    progress_parts.append(f"{days_done}d")
            elif journal_month:
                progress_parts.append(journal_month)
            progress = f" ({', '.join(progress_parts)})" if progress_parts else ""
            lines.append(f"  {_icon('seeder_ok')} seeder \u23f3 running {running_min}{progress}")
            if journal_line and not cur_sym:
                lines.append(f"    \u21b3 {journal_line}")
        else:
            age = results.get("seeder_age_min", "?")
            seeded = results.get("seeder_seeded", "?")
            elapsed = results.get("seeder_elapsed", "?")
            sym_detail = results.get("seeder_symbols", "")
            detail = f"{age} ago, {seeded}d seeded, {elapsed}s"
            if sym_detail:
                detail += f" ({sym_detail})"
            lines.append(f"  {_icon('seeder_ok')} seeder {detail}")
        failed = results.get("seeder_failed", "")
        if failed and failed != "0":
            lines.append(
                f"    \u274c {failed} symbol(s) failed\n"
                "      \u2192 journalctl --user -u opendeviationbar-seeder -n 30 | grep -i failed"
            )

    # OOM kill warnings (Issue #185)
    for svc in ("sidecar", "kintsugi"):
        oom_count = results.get(f"{svc}_oom")
        if oom_count:
            lines.append(
                f"  \U0001f4a5 {svc}: {oom_count} OOM kill(s) in 30min!\n"
                f"      \u2192 journalctl --user -u opendeviationbar-{svc} --since '30 min ago' | grep -i oom"
            )

    # Systemd override warnings (Issue #185)
    overrides_str = results.get("systemd_overrides", "")
    if overrides_str:
        lines.append(f"  \u26a0\ufe0f overrides active: {overrides_str}")

    # -- Version Drift (Issue #250) --
    if "version_drift" in results:
        installed = results.get("version_installed", "?")
        detail = results.get("version_drift_detail", "")
        if results["version_drift"] is True:
            if "hotfix" in detail:
                lines.append("")
                lines.append(f"<b>Version</b>: {detail} \U0001f529")
            else:
                lines.append("")
                lines.append(f"<b>Version</b>: v{installed} \u2705")
                if "auto_healed_pth" in results:
                    lines.append(
                        f"  \U0001f527 auto-healed: editable .pth removed"
                        f" (<code>{html.escape(results['auto_healed_pth'])}</code>)"
                    )
        else:
            lines.append("")
            lines.append("<b>Version</b>")
            lines.append(f"  \u274c <b>DRIFT</b>: {html.escape(detail)}")
            lines.append("      \u2192 <code>mise run deploy:bigblack</code>")

    # -- Kintsugi --
    lines.append("")
    lines.append("<b>Kintsugi</b>")
    tel_age = results.get("kintsugi_telemetry_age", "")
    tel_suffix = f" ({tel_age} ago)" if tel_age else ""
    lines.append(f"  {_icon('kintsugi_telemetry')} telemetry{tel_suffix}")
    if "kintsugi_stuck" in results:
        detail = results.get("kintsugi_stuck_detail", "")
        if results["kintsugi_stuck"] is True:
            active = results.get("kintsugi_active_repair")
            if active:
                lines.append(f"  🔄 repairing: {active}")
            else:
                lines.append(f"  {_icon('kintsugi_stuck')} no stuck repairs")
        else:
            lines.append(
                f"  {_icon('kintsugi_stuck')} stuck: {detail}\n"
                "      \u2192 expected &lt;30min; check REST rate limits or CH write latency"
            )
    recent_healed = results.get("kintsugi_recent_healed")
    if recent_healed:
        lines.append(f"  ✅ recent: {recent_healed}")
    repairs = results.get("kintsugi_repairs", "")
    bars_24h = results.get("kintsugi_bars_24h", "")
    if repairs:
        bars_suffix = f" | {bars_24h} bars" if bars_24h else ""
        lines.append(f"  {_icon('kintsugi_failures')} 24h: {repairs}{bars_suffix}")
    mem_pct = results.get("kintsugi_memory_pct", "")
    if mem_pct:
        lines.append(f"  {_icon('kintsugi_memory')} memory: {mem_pct}")
    # Shard queue depth
    total_shards = results.get("kintsugi_total_shards")
    if total_shards is not None:
        lines.append(f"  \U0001f4cb queue: shards={total_shards}")

    # -- Freshness (per-symbol, BTCUSDT first) --
    freshness = results.get("symbol_freshness", "")
    if freshness:
        lines.append("")
        lines.append("<b>Freshness</b>")
        for rline in str(freshness).split("\n"):
            lines.append(f"  {rline}")
        if "symbol_needs_backfill" in results:
            lines.append(
                "    \u2192 run: <code>mise run kintsugi:catchup</code>"
            )
        if "symbol_needs_seeding" in results:
            syms = results["symbol_needs_seeding"]
            lines.append(
                f"    \u2192 genesis seeding needed ({syms}):"
                f" <code>populate_cache_resumable(sym, effective_start, end)</code>"
            )

    # -- ClickHouse --
    if "ch_total_bars" in results:
        lines.append("")
        lines.append("<b>ClickHouse</b>")
        bars = results.get("ch_total_bars", "?")
        pairs = results.get("ch_pairs", "?")
        symbols = results.get("ch_symbols", "?")
        db_size = results.get("ch_db_size", "?")
        parts = results.get("ch_parts", "?")
        bars_24h = results.get("ch_bars_24h", "?")
        lines.append(f"  \U0001f4ca {bars} bars | {pairs} pairs | {symbols} symbols")
        lines.append(f"  \U0001f4be {db_size} ({parts} parts)")
        lines.append(f"  \U0001f4c8 {bars_24h} bars/24h")
        freshest = results.get("ch_freshest", "")
        stalest_pairs = results.get("ch_stalest_pairs", "")
        if freshest:
            lines.append(f"  \U0001f7e2 freshest: {freshest}")
        if stalest_pairs:
            lines.append("  \u23f3 stalest:")
            for sline in str(stalest_pairs).split("\n"):
                lines.append(f"    {sline}")
        # Pending mutations and active parts health
        pending = results.get("pending_mutations")
        active_parts_health = results.get("active_parts")
        if pending is not None or active_parts_health is not None:
            mut_icon = "\u2705" if results.get("mutations_healthy") is True else "\u26a0\ufe0f"
            mut_str = pending if pending is not None else "?"
            parts_str = active_parts_health if active_parts_health is not None else "?"
            lines.append(f"  {mut_icon} mutations: {mut_str} pending | {parts_str} active parts")

    # -- Stathera Integrity (TELEM-01: unified per-symbol, no classification) --
    if "stathera_overlaps" in results or "stathera_gaps" in results:
        lines.append("")
        lines.append("<b>Stathera</b>")
        overlap_count = results.get("stathera_overlap_count", "0")
        gap_count = results.get("stathera_gap_count", "0")
        overlap_delta = int(results.get("stathera_overlap_delta", "0"))
        gap_delta = int(results.get("stathera_gap_delta", "0"))
        has_overlaps = overlap_count != "0"
        has_gaps = gap_count != "0"

        def _delta_str(delta: int) -> str:
            if delta > 0:
                return f" (+{delta} new)"
            if delta < 0:
                return f" (-{abs(delta)} healed)"
            return ""

        if not has_overlaps and not has_gaps:
            lines.append("  bar-level continuity: clean")
        else:
            # Aggregate summary line
            overlap_icon = "" if overlap_delta > 0 else ""
            gap_icon = "" if gap_delta > 0 else ""
            summary_parts = []
            if has_overlaps:
                summary_parts.append(f"{overlap_icon} {overlap_count} overlaps{_delta_str(overlap_delta)}")
            if has_gaps:
                summary_parts.append(f"{gap_icon} {gap_count} gaps{_delta_str(gap_delta)}")
            lines.append(f"  {', '.join(summary_parts)}")

            # Per-symbol breakdown from stathera_per_symbol
            per_symbol: dict[str, dict] = results.get("stathera_per_symbol", {})
            if per_symbol:
                # Sort by gaps DESC, then overlaps DESC; show top 5
                sorted_pairs = sorted(
                    per_symbol.items(),
                    key=lambda kv: (-kv[1].get("gaps", 0), -kv[1].get("overlaps", 0)),
                )
                shown = sorted_pairs[:5]
                lines.append("  Per-symbol:")
                for pair_key, counts in shown:
                    lines.append(
                        f"    {pair_key}: {counts.get('gaps', 0)} gaps, "
                        f"{counts.get('overlaps', 0)} overlaps"
                    )
                remaining = len(sorted_pairs) - 5
                if remaining > 0:
                    lines.append(f"    + {remaining} more pairs with anomalies")

            # LOUD alert indicator (TELEM-04)
            if results.get("stathera_today_gap_alert"):
                lines.append("  LOUD alert sent for today's new gaps")

            # Fingerprint detail blocks (preserved)
            all_known = results.get("stathera_all_gaps_known", "False") == "True"
            fingerprints: list[dict] = results.get("stathera_gap_fingerprints", [])
            if all_known and fingerprints:
                for fp in fingerprints:
                    cause = fp.get("cause", "")
                    if cause == "pagination_bug_same_ms":
                        dropped = fp.get("dropped_trade_id", "?")
                        ts_ms = fp.get("boundary_ts_ms", "?")
                        sym = fp.get("symbol", "?")
                        thr = fp.get("threshold", "?")
                        lines.append(
                            f"\n  <b>Known root cause</b> ({sym}@{thr}): "
                            f"REST pagination bug (same-millisecond boundary)\n"
                            f"  Rust <code>fetch_aggtrades_rest()</code> advanced "
                            f"<code>startTime</code> by +1ms at page boundary\n"
                            f"  Trade <code>{dropped}</code> "
                            f"(T=<code>{ts_ms}ms</code>) was at the same ms as "
                            f"page-end trade -- silently dropped\n"
                            f"  Kintsugi cannot fix via REST (same bug path). "
                            f"Will self-heal when Binance Vision publishes the "
                            f"day's archive (~24-48h from trade time)\n"
                            f"  No manual action needed -- expected transient"
                        )

    # -- Yesterday Completion (TELEM-02) --
    yesterday_keys = [
        k for k in results if k.startswith("yesterday_") and not k.endswith("_detail")
        and k != "yesterday_completion"
    ]
    if yesterday_keys:
        lines.append("")
        lines.append("<b>Yesterday</b>")
        for symbol in HEARTBEAT_PRIORITY_SYMBOLS:
            key = f"yesterday_{symbol}"
            if key not in results:
                continue
            val = results[key]
            if val is True:
                lines.append(f"  \u2705 {symbol}: clean")
            elif val is False:
                detail = results.get(f"yesterday_{symbol}_detail", "unknown")
                lines.append(f"  \u274c {symbol}: mismatch -- {detail}")
            elif val == "no_orphan":
                lines.append(
                    f"  \u2139\ufe0f {symbol}: no orphan bar (fresh deploy or gap)"
                )
            elif val == "query_failed":
                lines.append(f"  \u26a0\ufe0f {symbol}: crossover query failed")

    # -- Junction Telemetry (TELEM-03) --
    if "junction_telemetry" in results:
        rest_tid = results.get("junction_rest_last_tid", "unavailable")
        ws_tid = results.get("junction_ws_first_tid", "unavailable")
        forming = results.get("junction_forming_bar", "unavailable")
        floors = results.get("junction_committed_floors", "unavailable")
        all_unavailable = all(
            v == "unavailable" for v in (rest_tid, ws_tid, forming, floors)
        )
        lines.append("")
        lines.append("<b>Junction</b>")
        if all_unavailable:
            lines.append("  ℹ️ sidecar telemetry unavailable")
        else:
            # Only show fields that have actual data
            if rest_tid != "unavailable":
                lines.append(f"  REST last TID: {rest_tid}")
            if ws_tid != "unavailable":
                lines.append(f"  WS first TID: {ws_tid}")
            if forming != "unavailable":
                lines.append(f"  Forming bar: {forming}")
            if floors != "unavailable":
                lines.append(f"  Floors: {floors}")
            # Note which fields are pending sidecar endpoint support
            missing = [n for n, v in [("WS TID", ws_tid), ("forming", forming), ("floors", floors)] if v == "unavailable"]
            if missing:
                lines.append(f"  ℹ️ {', '.join(missing)}: pending sidecar endpoint")

    # -- System --
    lines.append("")
    lines.append("<b>System</b>")
    free = results.get("disk_free_gb", "?")
    used_pct = results.get("disk_used_pct", "")
    disk_suffix = f" ({used_pct}% used)" if used_pct else ""
    lines.append(f"  {_icon('disk')} disk: {free} GB free{disk_suffix}")
    cb = results.get("circuit_breaker", "n/a")
    lines.append(f"  \U0001f512 breaker: {cb}")
    dl_count = results.get("dead_letter_count", "0")
    if dl_count != "0":
        dl_oldest = results.get("dead_letter_oldest", "?")
        lines.append(f"  \U0001f4ec dead letters: {dl_count} (oldest {dl_oldest})")

    # -- Resources (last 15 min peaks from Prometheus) --
    cpu_avg = results.get("resource_cpu_avg")
    ram_avg = results.get("resource_ram_used_gb")
    load_avg = results.get("resource_load_avg")
    if cpu_avg or ram_avg or load_avg:
        lines.append("")
        lines.append(f"<b>Resources</b> (last {RESOURCE_WINDOW_MINUTES}m)")
        if cpu_avg:
            cpu_abs = results.get("resource_cpu_abs_peak", "?")
            cpu_typ = results.get("resource_cpu_typ_peak", "?")
            lines.append(
                f"  \U0001f4bb CPU: avg {cpu_avg}% │ ⚡ abs {cpu_abs}% │ \U0001f4ca typ {cpu_typ}%"
            )
        if ram_avg:
            ram_abs = results.get("resource_ram_abs_peak_gb", "?")
            ram_typ = results.get("resource_ram_typ_peak_gb", "?")
            lines.append(
                f"  \U0001f9e0 RAM: avg {ram_avg}G │ ⚡ abs {ram_abs}G │ \U0001f4ca typ {ram_typ}G"
            )
        if load_avg:
            load_abs = results.get("resource_load_abs_peak", "?")
            load_typ = results.get("resource_load_typ_peak", "?")
            lines.append(
                f"  \u26a1 load: avg {load_avg} │ ⚡ abs {load_abs} │ \U0001f4ca typ {load_typ}"
            )

    return "\n".join(lines)
