#!/usr/bin/env python3
"""Run the once-nightly read-only Nightwatch maintenance payload."""

from __future__ import annotations

import subprocess
from pathlib import Path
from typing import Any

from recoil.pipeline.tools.autonomy import constants, events, resource_gate


REPORT_DIR = Path.home() / "logs/studio-autonomy"
_NIGHTWATCH_MODULE = "recoil.pipeline.tools.nightwatch"
_STEP_TIMEOUT_SECONDS = max(60, constants.TICK_LEASE_TTL // 4)


def _sentinel_path(night_id: str) -> Path:
    return constants.MAINT_DIR / f"{night_id}.done"


def _report_path(night_id: str) -> Path:
    return REPORT_DIR / f"maintenance-{night_id}.md"


def already_ran(night_id: str) -> bool:
    """Return whether the per-night maintenance sentinel exists."""
    try:
        _sentinel_path(night_id).stat()
        return True
    except FileNotFoundError:
        return False


def _is_dry_shadow(shadow: Any) -> bool:
    if shadow is False or shadow is None:
        return False
    if shadow is True:
        return True
    return str(shadow).strip().casefold() in {
        "1",
        "true",
        "yes",
        "dry",
        "dry-run",
        "dry_run",
        "shadow",
        "tier0",
        "tier-0",
        "tier_0",
        "0",
    }


def _command(*args: str) -> list[str]:
    return ["python3", "-m", _NIGHTWATCH_MODULE, *args]


def _combined_output(result: Any) -> str:
    return "\n".join(
        str(part)
        for part in (
            getattr(result, "stdout", ""),
            getattr(result, "stderr", ""),
            getattr(result, "returncode", ""),
        )
        if part is not None
    )


def _run_nightwatch(cmd: list[str], runner: Any) -> Any:
    try:
        return runner(
            cmd,
            capture_output=True,
            text=True,
            timeout=_STEP_TIMEOUT_SECONDS,
        )
    except subprocess.TimeoutExpired as exc:
        return subprocess.CompletedProcess(
            cmd,
            124,
            stdout=exc.stdout or "",
            stderr=exc.stderr or f"nightwatch step timed out after {_STEP_TIMEOUT_SECONDS}s",
        )


def _maybe_trip_rate_limit(night_id: str, result: Any) -> bool:
    output = _combined_output(result)
    if not resource_gate.is_rate_limit_error(output):
        return False
    resource_gate.trip_breaker(night_id, output)
    return True


def run_maintenance(
    run_id: str,
    night_id: str,
    *,
    shadow: Any = False,
    runner: Any = subprocess.run,
) -> dict[str, Any]:
    """Run ingest, optional bounded verify, and report for one autonomy night."""
    if already_ran(night_id):
        return {"skipped": "already_ran"}
    if resource_gate.breaker_tripped(night_id):
        return {"skipped": "night_breaker"}

    dry_shadow = _is_dry_shadow(shadow)
    steps: list[tuple[str, list[str]]] = [("ingest", _command("ingest"))]
    if not dry_shadow:
        steps.append(("verify", _command("verify", "--limit", "5")))
    steps.append(("report", _command("report")))

    ran: list[str] = []
    report_stdout = ""
    for step, cmd in steps:
        result = _run_nightwatch(cmd, runner)
        if _maybe_trip_rate_limit(night_id, result):
            return {"rate_limited": True}

        returncode = int(getattr(result, "returncode", 0) or 0)
        if returncode != 0:
            return {"failed": step, "returncode": returncode}

        ran.append(step)
        if step == "report":
            report_stdout = str(getattr(result, "stdout", "") or "")

    report_path = _report_path(night_id)
    report_path.parent.mkdir(parents=True, exist_ok=True)
    report_path.write_text(report_stdout, encoding="utf-8")

    sentinel = _sentinel_path(night_id)
    sentinel.parent.mkdir(parents=True, exist_ok=True)
    sentinel.touch()

    events.emit(
        "maintenance_ran",
        run_id=run_id,
        night_id=night_id,
        report_path=str(report_path),
    )

    return {
        "ran": ran,
        "night_id": night_id,
        "report_path": str(report_path),
        "skipped_verify": dry_shadow,
    }
