from __future__ import annotations

import json
import time
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

HEARTBEAT_MAX_AGE_SECONDS = 180


@dataclass(frozen=True)
class CandidateSpec:
    strategy_id: str
    module: str
    strategy_version: str
    coins: tuple[str, ...] = ("BTC", "ETH", "SOL", "LINK")
    paper_only: bool = True
    enabled: bool = True


CANDIDATES: dict[str, CandidateSpec] = {
    "candidate_v77_trend_retest_anti_chase_long": CandidateSpec("candidate_v77_trend_retest_anti_chase_long", "src.tools.v77_trend_retest_runtime", "v77.1.0"),
    "research_v77_bear_trend_retest_short": CandidateSpec("research_v77_bear_trend_retest_short", "src.tools.v77_trend_retest_runtime", "v77.1.0"),
    "research_v77_2_relative_strength_momentum_continuation": CandidateSpec("research_v77_2_relative_strength_momentum_continuation", "src.tools.v77_trend_retest_runtime", "v77.2.0", coins=("BTC", "ETH", "LINK")),
    "research_v78_regime_router": CandidateSpec("research_v78_regime_router", "src.tools.v78_research_runtime", "v78.1.0"),
    "research_v78_2_confirmed_range_reversion": CandidateSpec("research_v78_2_confirmed_range_reversion", "src.tools.v78_2_range_runtime", "v78.2.0", coins=("BTC", "ETH", "SOL", "LINK", "HYPE")),
    "research_v78_liquidation_sweep_reversal": CandidateSpec("research_v78_liquidation_sweep_reversal", "src.tools.v78_research_runtime", "v78.1.0"),
    "research_v78_market_neutral_relative_value": CandidateSpec("research_v78_market_neutral_relative_value", "src.tools.v78_research_runtime", "v78.1.0", enabled=False),
}


@dataclass(frozen=True)
class ProcessAssessment:
    strategy_id: str
    state: str
    pid: int | None
    process_exists: bool
    command_ok: bool
    environment_ok: bool
    heartbeat_fresh: bool
    heartbeat_age_seconds: float | None
    restart_allowed: bool
    blockers: tuple[str, ...]

    def to_dict(self) -> dict[str, Any]:
        raw = asdict(self)
        raw["blockers"] = list(self.blockers)
        return raw


def _latest_health(path: Path, now: float) -> tuple[bool, float | None, list[str]]:
    if not path.exists():
        return False, None, ["heartbeat_missing"]
    latest: dict[str, Any] | None = None
    invalid = False
    for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        if not line.strip():
            continue
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            invalid = True
            continue
        if isinstance(row, dict):
            latest = row
    blockers: list[str] = ["heartbeat_jsonl_invalid"] if invalid else []
    if not latest:
        blockers.append("heartbeat_missing")
        return False, None, blockers
    try:
        stamp = datetime.fromisoformat(str(latest.get("timestamp")).replace("Z", "+00:00"))
        age = max(0.0, now - stamp.timestamp())
    except Exception:
        blockers.append("heartbeat_timestamp_invalid")
        return False, None, blockers
    if age > HEARTBEAT_MAX_AGE_SECONDS:
        blockers.append("heartbeat_stale")
    if latest.get("status") != "ok":
        blockers.append("heartbeat_status_not_ok")
    return not blockers, age, blockers


def inspect_candidate(
    spec: CandidateSpec,
    *,
    runtime_root: str | Path,
    proc_root: str | Path = "/proc",
    now: float | None = None,
) -> ProcessAssessment:
    runtime = Path(runtime_root) / spec.strategy_id
    proc_base = Path(proc_root)
    blockers: list[str] = []
    if not spec.enabled:
        return ProcessAssessment(spec.strategy_id, "disabled", None, False, False, False, False, None, False, ("candidate_disabled",))
    try:
        pid = int((runtime / "bot.pid").read_text(encoding="utf-8").strip())
    except Exception:
        pid = None
    proc = proc_base / str(pid) if pid is not None else None
    process_exists = bool(proc and proc.exists())
    command_ok = False
    environment_ok = False
    if process_exists and proc is not None:
        try:
            command = (proc / "cmdline").read_bytes().decode("utf-8", "ignore")
            environment = (proc / "environ").read_bytes().decode("utf-8", "ignore")
            command_ok = spec.module in command and f"--strategy-id\x00{spec.strategy_id}" in command
            environment_ok = all((
                f"CTB_STRATEGY_ID={spec.strategy_id}" in environment,
                "CTB_PAPER_TRADING=true" in environment,
                "CTB_DRY_RUN=false" in environment,
                "CTB_LIVE_TRADING_ALLOWED=false" in environment,
                "CTB_LIVE_ORDER_ALLOWED=false" in environment,
                "HL_MAINNET_SIGNED_ACTION=false" in environment,
            ))
        except Exception:
            blockers.append("process_metadata_unreadable")
    if process_exists and not command_ok:
        blockers.append("process_command_mismatch")
    if process_exists and not environment_ok:
        blockers.append("process_environment_unsafe")
    heartbeat_fresh, heartbeat_age, health_blockers = _latest_health(runtime / "runtime_health.jsonl", now if now is not None else time.time())
    blockers.extend(health_blockers)

    if not process_exists:
        state = "missing"
        restart_allowed = True
        blockers.insert(0, "process_missing")
    elif not command_ok or not environment_ok:
        state = "unsafe_or_unclear"
        restart_allowed = False
    elif not heartbeat_fresh:
        state = "stale"
        restart_allowed = False
    else:
        state = "healthy"
        restart_allowed = False
    return ProcessAssessment(spec.strategy_id, state, pid, process_exists, command_ok, environment_ok, heartbeat_fresh, heartbeat_age, restart_allowed, tuple(dict.fromkeys(blockers)))
