"""
Smoke Check — runtime invariant validation for the gold_autotrade system.

Runs fast (<2s) checks against live state files. Designed to run:
- On startup (launcher gate)
- On a cron every 5 minutes
- After any code change / deploy

Returns {"ok": bool, "checks": [...], "failures": [...]}.
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from typing import Any, Optional


def _resolve_app_dir() -> Optional[Path]:
    env = os.environ.get("LOBSTERQUANT_APP_DIR", "").strip()
    if env:
        return Path(env)
    try:
        import config_manager
        d = config_manager.get_app_dir()
        return Path(d) if d else None
    except Exception:
        return None


def _load_json(path: Path) -> dict[str, Any]:
    with open(path) as f:
        return json.load(f)


# ---------------------------------------------------------------------------
# Invariant 1: HOLD signals must not leak CREATED state entries
# Regression guard for the bug where every pipeline tick created a CREATED
# entry for HOLD signals.
# ---------------------------------------------------------------------------
def _signal_items(signals: Any) -> list[dict]:
    """Normalize signals to a list of dicts (handles both list and dict-by-id)."""
    if isinstance(signals, dict):
        return [v for v in signals.values() if isinstance(v, dict)]
    if isinstance(signals, list):
        return [s for s in signals if isinstance(s, dict)]
    return []


def check_no_hold_created(app_dir: Path) -> dict:
    path = app_dir / "data" / "execution_state.json"
    if not path.exists():
        return {"check": "smoke/no_hold_created", "status": "SKIP", "reason": "file missing"}
    try:
        data = _load_json(path)
        items = _signal_items(data.get("signals"))
        stale = [s for s in items
                 if str(s.get("action", "")).upper() == "HOLD"
                 and str(s.get("state", "")).upper() == "CREATED"]
        if stale:
            return {"check": "smoke/no_hold_created", "status": "FAIL",
                    "reason": f"{len(stale)} HOLD signals stuck in CREATED state"}
        return {"check": "smoke/no_hold_created", "status": "PASS",
                "detail": f"{len(items)} total signals, 0 HOLD CREATED"}
    except Exception as e:
        return {"check": "smoke/no_hold_created", "status": "FAIL", "reason": str(e)}


# ---------------------------------------------------------------------------
# Invariant 2: CREATED signal count for non-HOLD actions — should be 0
# Any non-HOLD CREATED signal means the dispatch pipeline is stuck.
# ---------------------------------------------------------------------------
def check_no_stale_created(app_dir: Path) -> dict:
    path = app_dir / "data" / "execution_state.json"
    if not path.exists():
        return {"check": "smoke/no_stale_created", "status": "SKIP", "reason": "file missing"}
    try:
        data = _load_json(path)
        items = _signal_items(data.get("signals"))
        stale = [s for s in items
                 if str(s.get("action", "")).upper() != "HOLD"
                 and str(s.get("state", "")).upper() == "CREATED"]
        if stale:
            ids = [s.get("id") or s.get("signal_id") or "?" for s in stale[:5]]
            return {"check": "smoke/no_stale_created", "status": "FAIL",
                    "reason": f"{len(stale)} non-HOLD signals stuck in CREATED (first 5: {ids})"}
        return {"check": "smoke/no_stale_created", "status": "PASS",
                "detail": f"{len(items)} total signals, 0 stale CREATED"}
    except Exception as e:
        return {"check": "smoke/no_stale_created", "status": "FAIL", "reason": str(e)}


# ---------------------------------------------------------------------------
# Invariant 3: balance/equity must be valid when position is open
# Regression guard for the bug where balance/equity showed None.
# ---------------------------------------------------------------------------
def check_balance_integrity(app_dir: Path) -> dict:
    path = app_dir / "data" / "bridge" / "state" / "agent_state.json"
    if not path.exists():
        return {"check": "smoke/balance_integrity", "status": "SKIP", "reason": "file missing"}
    try:
        state = _load_json(path)
        pos_open = bool(state.get("position_open", False))
        if not pos_open:
            return {"check": "smoke/balance_integrity", "status": "PASS", "detail": "no open position"}
        balance = state.get("balance")
        equity = state.get("equity")
        if balance is None or equity is None:
            return {"check": "smoke/balance_integrity", "status": "FAIL",
                    "reason": f"position_open=True but balance={balance} equity={equity}"}
        if float(balance) <= 0:
            return {"check": "smoke/balance_integrity", "status": "FAIL",
                    "reason": f"balance={balance} <= 0 with open position"}
        if float(equity) <= 0:
            return {"check": "smoke/balance_integrity", "status": "FAIL",
                    "reason": f"equity={equity} <= 0 with open position"}
        return {"check": "smoke/balance_integrity", "status": "PASS",
                "detail": f"balance={balance} equity={equity}"}
    except Exception as e:
        return {"check": "smoke/balance_integrity", "status": "FAIL", "reason": str(e)}


# ---------------------------------------------------------------------------
# Invariant 4: position_lifecycle must be consistent with agent_state
# If agent says position_open, lifecycle must not be flat/orphaned.
# If agent says no position, lifecycle must not be open.
# ---------------------------------------------------------------------------
def check_lifecycle_consistency(app_dir: Path) -> dict:
    agent_path = app_dir / "data" / "bridge" / "state" / "agent_state.json"
    lifecycle_path = app_dir / "data" / "position_lifecycle.json"
    if not agent_path.exists() or not lifecycle_path.exists():
        return {"check": "smoke/lifecycle_consistency", "status": "SKIP", "reason": "file(s) missing"}
    try:
        agent = _load_json(agent_path)
        lifecycle = _load_json(lifecycle_path)
        pos_open_agent = bool(agent.get("position_open", False))
        current_lc = lifecycle.get("current", {}) if isinstance(lifecycle, dict) else {}
        lc_state = str(current_lc.get("status", "flat") or "flat").lower()

        if pos_open_agent and lc_state == "flat":
            return {"check": "smoke/lifecycle_consistency", "status": "FAIL",
                    "reason": "agent says position_open but lifecycle is flat (orphaned)"}
        if not pos_open_agent and lc_state == "open":
            return {"check": "smoke/lifecycle_consistency", "status": "FAIL",
                    "reason": "agent says no position but lifecycle says open"}
        return {"check": "smoke/lifecycle_consistency", "status": "PASS",
                "detail": f"agent_open={pos_open_agent} lifecycle_state={lc_state}"}
    except Exception as e:
        return {"check": "smoke/lifecycle_consistency", "status": "FAIL", "reason": str(e)}


# ---------------------------------------------------------------------------
# Invariant 5: macro binding_direction must agree with allowed_side
# Regression guard for structural guard bypass bug.
# ---------------------------------------------------------------------------
def check_binding_allowed_side(app_dir: Path) -> dict:
    path = app_dir / "data" / "agent_decision_contract.json"
    if not path.exists():
        return {"check": "smoke/binding_allowed_side", "status": "SKIP", "reason": "file missing"}
    try:
        contract = _load_json(path)
        macro = contract.get("macro", {}) if isinstance(contract.get("macro", {}), dict) else {}
        degraded = bool(macro.get("degraded", True))
        macro_authority = str(macro.get("macro_authority", "DISABLED") or "DISABLED").upper()
        binding = str(macro.get("binding_direction", "NEUTRAL") or "NEUTRAL").upper()
        allowed_side = str(macro.get("allowed_side", "BOTH") or "BOTH").upper()
        if degraded or macro_authority != "HARD_BIND" or binding not in ("LONG", "SHORT"):
            return {"check": "smoke/binding_allowed_side", "status": "PASS",
                    "detail": f"degraded={degraded} authority={macro_authority} binding={binding}"}
        expected = "BUY" if binding == "LONG" else "SELL"
        if allowed_side == expected or allowed_side == "BOTH":
            return {"check": "smoke/binding_allowed_side", "status": "PASS",
                    "detail": f"authority={macro_authority} binding={binding} allowed_side={allowed_side}"}
        return {"check": "smoke/binding_allowed_side", "status": "FAIL",
                "reason": f"authority={macro_authority} binding={binding} but allowed_side={allowed_side} (expected {expected})"}
    except Exception as e:
        return {"check": "smoke/binding_allowed_side", "status": "FAIL", "reason": str(e)}


# ---------------------------------------------------------------------------
# Invariant 6: close_report_dedupe must not overflow
# Soft cap at 1000 to prevent unbounded growth.
# ---------------------------------------------------------------------------
def check_dedupe_not_overflowing(app_dir: Path) -> dict:
    path = app_dir / "data" / "bridge" / "state" / "agent_state.json"
    if not path.exists():
        return {"check": "smoke/dedupe_overflow", "status": "SKIP", "reason": "file missing"}
    try:
        state = _load_json(path)
        dedupe = state.get("close_report_dedupe")
        if dedupe is None:
            return {"check": "smoke/dedupe_overflow", "status": "PASS", "detail": "no dedupe field"}
        if isinstance(dedupe, list):
            count = len(dedupe)
        else:
            count = int(dedupe)
        if count > 1000:
            return {"check": "smoke/dedupe_overflow", "status": "FAIL",
                    "reason": f"close_report_dedupe size={count} > 1000 (possible unbounded growth)"}
        return {"check": "smoke/dedupe_overflow", "status": "PASS", "detail": f"dedupe_size={count}"}
    except Exception as e:
        return {"check": "smoke/dedupe_overflow", "status": "FAIL", "reason": str(e)}


# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
def run_smoke_check(app_dir: Optional[Path] = None) -> dict[str, Any]:
    if app_dir is None:
        app_dir = _resolve_app_dir()
    if app_dir is None:
        return {"ok": False, "checks": [], "failures": ["APP_DIR not resolved"]}

    checks = [
        check_no_hold_created(app_dir),
        check_no_stale_created(app_dir),
        check_balance_integrity(app_dir),
        check_lifecycle_consistency(app_dir),
        check_binding_allowed_side(app_dir),
        check_dedupe_not_overflowing(app_dir),
    ]

    failures = [c for c in checks if c.get("status") == "FAIL"]
    ok = len(failures) == 0

    return {
        "ok": ok,
        "total_checks": len(checks),
        "passed": sum(1 for c in checks if c.get("status") == "PASS"),
        "failed": len(failures),
        "skipped": sum(1 for c in checks if c.get("status") == "SKIP"),
        "checks": checks,
        "failures": [f"{c['check']}: {c.get('reason', 'unknown')}" for c in failures],
    }


if __name__ == "__main__":
    result = run_smoke_check()
    print(json.dumps(result, indent=2, ensure_ascii=False))
    sys.exit(0 if result["ok"] else 1)
