#!/usr/bin/env python3
"""Fail-fast validation for kintsugi fixes against real ClickHouse data.
# Issue #158: Stathera trade-ID audit (replaces legacy time-based probes)

Runs 8 probes to verify that kintsugi fixes (Stathera audit,
"clean" result state, watchdog ping, rate-limit exclusion, monitoring pipeline)
work correctly before restarting the kintsugi daemon.

Usage:
    ssh bigblack "cd ~/opendeviationbar-py && .venv/bin/python3 scripts/validate_kintsugi_fixes.py"

Exit codes:
    0 - all validations pass
    1 - some validations failed (investigate)
    2 - critical infrastructure down
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
import time
from datetime import UTC, datetime
from pathlib import Path

ARTIFACTS_DIR = Path(__file__).resolve().parent.parent / "artifacts"


def _run(
    cmd: list[str], timeout: int = 30, *, local_mode: bool = False
) -> subprocess.CompletedProcess[str]:  # SSoT-OK: script-local helper
    env = None
    if local_mode:
        env = {**os.environ, "OPENDEVIATIONBAR_MODE": "local"}
    return subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=timeout,
        check=False,
        env=env,
    )


# ---------------------------------------------------------------------------
# Probes
# ---------------------------------------------------------------------------


def probe_clickhouse_connectivity() -> dict:
    """Probe 1: ClickHouse connectivity prerequisite."""
    try:
        r = _run(["curl", "-sf", "http://localhost:8123/?query=SELECT+1"], timeout=10)
        if r.returncode == 0 and r.stdout.strip() == "1":
            return {"ok": True}
        return {"ok": False, "error": f"unexpected response: {r.stdout.strip()!r}"}
    except subprocess.TimeoutExpired:
        return {"ok": False, "error": "timeout (10s)"}
    except OSError as e:
        return {"ok": False, "error": str(e)}


def probe_kintsugi_log_schema() -> dict:
    """Probe 2: kintsugi_log accepts 'clean' result state (round-trip test)."""
    insert_query = (
        "INSERT INTO opendeviationbar_cache.kintsugi_log "
        "(symbol, threshold_decimal_bps, priority, "
        "gap_start_ms, gap_end_ms, gap_hours, "
        "action, result, ariadne_from_id, ouroboros_splits, "
        "bars_written, duration_seconds, error_message) "
        "VALUES "
        "('__PROBE__', 0, '', 0, 0, 0.0, "
        "'probe', 'clean', 0, 0, 0, 0.0, '')"
    )
    select_query = (
        "SELECT count() FROM opendeviationbar_cache.kintsugi_log "
        "WHERE symbol = '__PROBE__' AND result = 'clean'"
    )
    # kintsugi_log has no PARTITION BY clause (single implicit partition).
    # ALTER TABLE DELETE is correct here -- table is tiny (90-day TTL).
    # DELETE IN PARTITION cannot be used without an explicit partition key.
    delete_query = (
        "ALTER TABLE opendeviationbar_cache.kintsugi_log "
        "DELETE WHERE symbol = '__PROBE__' "
        "SETTINGS mutations_sync = 1"
    )
    try:
        # Cleanup any leftover probe data from prior runs
        _run(["curl", "-sf", "-d", delete_query, "http://localhost:8123/"], timeout=10)

        # Insert
        r = _run(
            ["curl", "-sf", "-d", insert_query, "http://localhost:8123/"], timeout=10
        )
        if r.returncode != 0:
            return {"ok": False, "error": f"INSERT failed: {r.stderr.strip()[:200]}"}

        # Select (POST to avoid URL encoding issues) — expect >= 1
        r = _run(
            ["curl", "-sf", "-d", select_query, "http://localhost:8123/"], timeout=10
        )
        count = int(r.stdout.strip()) if r.returncode == 0 and r.stdout.strip() else 0
        if count < 1:
            return {"ok": False, "error": f"SELECT returned: {r.stdout.strip()!r}"}

        # Cleanup
        _run(["curl", "-sf", "-d", delete_query, "http://localhost:8123/"], timeout=10)

        return {"ok": True}
    except (subprocess.TimeoutExpired, OSError) as e:
        return {"ok": False, "error": str(e)}


def probe_stathera_audit() -> dict:
    """Probe 3: Stathera trade-ID audit runs and produces valid JSON."""
    try:
        r = _run(
            [
                sys.executable,
                "-W",
                "ignore",
                "scripts/detect_gaps.py",
                "--json",
                "--max-stale-hours",
                "72",
            ],
            timeout=120,
            local_mode=True,
        )

        if r.returncode == 2:
            return {"ok": False, "error": "detect_gaps connection error"}

        try:
            data = json.loads(r.stdout)
        except (json.JSONDecodeError, ValueError):
            return {"ok": False, "error": "failed to parse detect_gaps output"}

        gap_count = data.get("stathera_gap_count", -1)
        overlap_count = data.get("stathera_overlap_count", -1)
        checked = data.get("checked_pairs", 0)

        return {
            "ok": checked > 0 and gap_count >= 0 and overlap_count >= 0,
            "checked_pairs": checked,
            "stathera_gaps": gap_count,
            "stathera_overlaps": overlap_count,
        }
    except (subprocess.TimeoutExpired, OSError) as e:
        return {"ok": False, "error": str(e)}


def probe_shard_count_sanity() -> dict:
    """Probe 4: Total Stathera anomalies + stale pairs is manageable (<100)."""
    try:
        r = _run(
            [
                sys.executable,
                "-W",
                "ignore",
                "scripts/detect_gaps.py",
                "--json",
                "--max-stale-hours",
                "72",
            ],
            timeout=120,
            local_mode=True,
        )
        if r.returncode == 2:
            return {"ok": False, "error": "detect_gaps connection error"}

        try:
            data = json.loads(r.stdout)
        except (json.JSONDecodeError, ValueError):
            return {"ok": False, "error": "failed to parse output"}

        # Issue #158: Stathera trade-ID gaps + stale pairs
        gap_count = data.get("stathera_gap_count", 0)
        overlap_count = data.get("stathera_overlap_count", 0)
        stale = data.get("stale_pairs", [])
        total = gap_count + overlap_count + len(stale)

        return {
            "ok": total < 100,
            "total_shards": total,
            "stathera_gaps": gap_count,
            "stathera_overlaps": overlap_count,
            "stale_pairs": len(stale),
        }
    except (subprocess.TimeoutExpired, OSError) as e:
        return {"ok": False, "error": str(e)}


def probe_watchdog_ping() -> dict:
    """Probe 5: _sd_notify callable without exception."""
    try:
        from opendeviationbar.kintsugi import _sd_notify

        # Call with WATCHDOG=1 — no-op if NOTIFY_SOCKET not set
        _sd_notify("WATCHDOG=1")
        return {
            "ok": True,
            "note": "NOTIFY_SOCKET not set"
            if not os.environ.get("NOTIFY_SOCKET")
            else "pinged",
        }
    except (ImportError, OSError, RuntimeError) as e:
        return {"ok": False, "error": str(e)}


def probe_rate_limit_clean_bypass() -> dict:
    """Probe 6: 'clean' results don't count toward rate limit."""
    query = (
        "SELECT count() FROM opendeviationbar_cache.kintsugi_log "
        "WHERE symbol = '__PROBE_RL__' "
        "AND result != 'clean' "
        "AND timestamp > now() - INTERVAL 24 HOUR"
    )
    # Insert 3 "clean" entries — should NOT trigger rate limit
    insert_query = (
        "INSERT INTO opendeviationbar_cache.kintsugi_log "
        "(symbol, threshold_decimal_bps, priority, "
        "gap_start_ms, gap_end_ms, gap_hours, "
        "action, result, ariadne_from_id, ouroboros_splits, "
        "bars_written, duration_seconds, error_message) "
        "VALUES "
        "('__PROBE_RL__', 0, '', 999, 0, 0.0, 'probe', 'clean', 0, 0, 0, 0.0, ''),"
        "('__PROBE_RL__', 0, '', 999, 0, 0.0, 'probe', 'clean', 0, 0, 0, 0.0, ''),"
        "('__PROBE_RL__', 0, '', 999, 0, 0.0, 'probe', 'clean', 0, 0, 0, 0.0, '')"
    )
    # kintsugi_log has no PARTITION BY clause (single implicit partition).
    # ALTER TABLE DELETE is correct here -- table is tiny (90-day TTL).
    # DELETE IN PARTITION cannot be used without an explicit partition key.
    delete_query = (
        "ALTER TABLE opendeviationbar_cache.kintsugi_log "
        "DELETE WHERE symbol = '__PROBE_RL__' "
        "SETTINGS mutations_sync = 1"
    )
    try:
        # Insert 3 clean entries
        r = _run(
            ["curl", "-sf", "-d", insert_query, "http://localhost:8123/"], timeout=10
        )
        if r.returncode != 0:
            return {"ok": False, "error": f"INSERT failed: {r.stderr.strip()[:200]}"}

        # Check count excluding 'clean' — should be 0 (POST to avoid URL encoding)
        r = _run(["curl", "-sf", "-d", query, "http://localhost:8123/"], timeout=10)
        count = int(r.stdout.strip()) if r.returncode == 0 and r.stdout.strip() else -1

        # Cleanup
        _run(["curl", "-sf", "-d", delete_query, "http://localhost:8123/"], timeout=10)

        return {
            "ok": count == 0,
            "non_clean_count": count,
            "note": "3 clean entries inserted, rate limit query returns 0"
            if count == 0
            else "FAIL",
        }
    except (subprocess.TimeoutExpired, OSError, ValueError) as e:
        return {"ok": False, "error": str(e)}


def probe_dry_run_pass() -> dict:
    """Probe 7: kintsugi_pass(dry_run=True) completes without exception."""
    try:
        from opendeviationbar.kintsugi import kintsugi_pass

        t0 = time.monotonic()
        results = kintsugi_pass(dry_run=True)
        elapsed = round(time.monotonic() - t0, 1)
        return {
            "ok": True,
            "duration_s": elapsed,
            "note": f"dry_run returned {len(results)} results",
        }
    except (ImportError, OSError, RuntimeError, ConnectionError, ValueError) as e:
        return {"ok": False, "error": str(e)}


def probe_monitoring_pipeline() -> dict:
    """Probe 8: Monitoring pipeline self-test."""
    checks: dict[str, object] = {}

    # NDJSON log writable
    ndjson = Path.home() / "opendeviationbar-py" / "logs" / "kintsugi.ndjson"
    checks["ndjson_exists"] = ndjson.exists()
    if ndjson.exists():
        checks["ndjson_writable"] = os.access(ndjson, os.W_OK)
        checks["ndjson_size_mb"] = round(ndjson.stat().st_size / 1024 / 1024, 1)

    # Telegram token configured (env var or EnvironmentFile for systemd)
    token_in_env = bool(os.environ.get("OPENDEVIATIONBAR_TELEGRAM_TOKEN"))
    env_file = Path.home() / "opendeviationbar-py" / "deploy" / "opendeviationbar.local.env"
    token_in_file = False
    if not token_in_env and env_file.exists():
        import contextlib

        with contextlib.suppress(OSError):
            token_in_file = "OPENDEVIATIONBAR_TELEGRAM_TOKEN=" in env_file.read_text()
    checks["telegram_configured"] = token_in_env or token_in_file

    # kintsugi_log table exists and queryable
    r = _run(
        ["curl", "-sf", "-d",
         "SELECT count() FROM opendeviationbar_cache.kintsugi_log",
         "http://localhost:8123/"],
        timeout=10,
    )
    checks["kintsugi_log_queryable"] = (
        r.returncode == 0 and r.stdout.strip().isdigit()
    )

    # Heartbeat state file accessible
    state_dir = Path.home() / ".cache" / "opendeviationbar" / "heartbeat"
    checks["heartbeat_state_dir"] = state_dir.exists()

    ok = all(v for v in checks.values() if isinstance(v, bool))
    return {"ok": ok, **checks}


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------


def run_all_probes() -> dict:
    """Run all 8 probes and build report."""
    import socket

    probes = {
        "1_clickhouse_connectivity": probe_clickhouse_connectivity,
        "2_kintsugi_log_schema": probe_kintsugi_log_schema,
        "3_stathera_audit": probe_stathera_audit,
        "4_shard_count_sanity": probe_shard_count_sanity,
        "5_watchdog_ping": probe_watchdog_ping,
        "6_rate_limit_clean_bypass": probe_rate_limit_clean_bypass,
        "7_dry_run_pass": probe_dry_run_pass,
        "8_monitoring_pipeline": probe_monitoring_pipeline,
    }

    # Probe 1 is prerequisite — fail fast if ClickHouse is down
    ch_result = probe_clickhouse_connectivity()
    if not ch_result.get("ok"):
        return {
            "timestamp": datetime.now(UTC).isoformat(),
            "hostname": socket.gethostname(),
            "status": "critical",
            "exit_code": 2,
            "probes": {"1_clickhouse_connectivity": ch_result},
            "summary": "ClickHouse unreachable — cannot validate",
        }

    results = {"1_clickhouse_connectivity": ch_result}
    for name, fn in list(probes.items())[1:]:  # Skip probe 1 (already ran)
        print(f"  Running {name}...", file=sys.stderr, flush=True)
        try:
            results[name] = fn()
        except (
            ImportError,
            OSError,
            RuntimeError,
            ConnectionError,
            ValueError,
            subprocess.TimeoutExpired,
        ) as e:
            results[name] = {"ok": False, "error": f"probe crashed: {e}"}

    all_ok = all(r.get("ok", False) for r in results.values())
    failed_probes = [k for k, v in results.items() if not v.get("ok", False)]

    return {
        "timestamp": datetime.now(UTC).isoformat(),
        "hostname": socket.gethostname(),
        "status": "pass" if all_ok else "fail",
        "exit_code": 0 if all_ok else 1,
        "probes": results,
        "summary": (
            "all 8 probes pass"
            if all_ok
            else f"{len(failed_probes)} probe(s) failed: {', '.join(failed_probes)}"
        ),
    }


def main() -> int:
    os.chdir(Path(__file__).resolve().parent.parent)

    print("Kintsugi fix validation starting...", file=sys.stderr, flush=True)
    report = run_all_probes()
    exit_code = report["exit_code"]

    # Write artifacts
    ARTIFACTS_DIR.mkdir(exist_ok=True)
    ts = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
    report_path = ARTIFACTS_DIR / f"kintsugi_validation_{ts}.json"
    report_path.write_text(json.dumps(report, indent=2) + "\n")

    latest = ARTIFACTS_DIR / "kintsugi_validation_latest.json"
    latest.unlink(missing_ok=True)
    latest.symlink_to(report_path.name)

    # Print to stdout
    print(json.dumps(report, indent=2))

    # Human-friendly summary to stderr
    status_icon = {"pass": "OK", "fail": "FAIL", "critical": "CRIT"}
    print(
        f"\n[{status_icon.get(report['status'], '?')}] "
        f"{report['status'].upper()}: {report['summary']}",
        file=sys.stderr,
    )

    return exit_code


if __name__ == "__main__":
    sys.exit(main())
