#!/usr/bin/env python3
"""Collect live Leo restart-survival proof.

This harness intentionally restarts only ``leoclean-gateway.service`` when
``--execute-restart`` is supplied, then proves the gateway came back, canonical
KB counts did not change, and the no-post direct-claim handler still answers.
It never posts to Telegram and never runs production DB apply.
"""

from __future__ import annotations

import argparse
import json
import re
import shlex
import subprocess
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

REPORT_DIR = Path("docs/reports/leo-working-state-20260709")
DEFAULT_OUT = REPORT_DIR / "leo-restart-survival-proof-current.json"
DEFAULT_MARKDOWN_OUT = REPORT_DIR / "leo-restart-survival-proof-current.md"
DEFAULT_SSH_TARGET = "root@77.42.65.182"
DEFAULT_SSH_KEY = Path.home() / ".ssh/livingip_hetzner_20260604_ed25519"
REMOTE_REPO = "/opt/teleo-eval/workspaces/deploy-infra"
REMOTE_HANDLER_REPORT = "/tmp/leo-restart-survival-direct-claim-current.json"
EXPECTED_COUNTS = [
    "public.claims",
    "public.sources",
    "public.claim_evidence",
    "public.claim_edges",
    "kb_stage.kb_proposals",
]

SECRET_PATTERNS = [
    re.compile(r"\b\d{6,}:[A-Za-z0-9_-]{20,}\b"),
    re.compile(r"(Authorization: Bearer )([A-Za-z0-9._-]+)", re.IGNORECASE),
    re.compile(r"((?:api[_-]?key|token|secret|password)[=:]\s*)([A-Za-z0-9._-]+)", re.IGNORECASE),
]


@dataclass(frozen=True)
class CommandResult:
    returncode: int
    stdout: str
    stderr: str


Runner = Callable[[list[str], str, int], CommandResult]


def subprocess_runner(command: list[str], input_text: str, timeout: int) -> CommandResult:
    proc = subprocess.run(command, input=input_text, text=True, capture_output=True, timeout=timeout, check=False)
    return CommandResult(proc.returncode, proc.stdout, proc.stderr)


def redact(text: str) -> str:
    result = text
    for pattern in SECRET_PATTERNS:
        if pattern.groups >= 2:
            result = pattern.sub(r"\1[REDACTED]", result)
        else:
            result = pattern.sub("[REDACTED_TOKEN]", result)
    return result


def parse_last_json_line(text: str) -> dict[str, Any]:
    cleaned = redact(text).strip()
    for line in reversed(cleaned.splitlines()):
        candidate = line.strip()
        if candidate.startswith("{"):
            return json.loads(candidate)
    raise ValueError("command output contained no JSON object line")


def ssh_command(args: argparse.Namespace, remote_command: str) -> list[str]:
    return [
        "ssh",
        "-i",
        str(args.ssh_key),
        "-o",
        "BatchMode=yes",
        "-o",
        "StrictHostKeyChecking=accept-new",
        "-o",
        f"ConnectTimeout={args.connect_timeout}",
        args.ssh_target,
        remote_command,
    ]


def db_counts_sql() -> str:
    return """
select jsonb_build_object(
  'public.claims', (select count(*) from public.claims),
  'public.sources', (select count(*) from public.sources),
  'public.claim_evidence', (select count(*) from public.claim_evidence),
  'public.claim_edges', (select count(*) from public.claim_edges),
  'kb_stage.kb_proposals', (select count(*) from kb_stage.kb_proposals)
)::text;
""".strip()


def remote_readback_command(label: str) -> str:
    script = f"""
import json
import subprocess

def run(command, *, input_text=None):
    return subprocess.run(command, input=input_text, text=True, capture_output=True, check=False)

def kv(text):
    out = {{}}
    for line in text.splitlines():
        if "=" in line:
            key, value = line.split("=", 1)
            out[key] = value.strip()
    return out

service = run([
    "systemctl",
    "show",
    "leoclean-gateway.service",
    "-p", "ActiveState",
    "-p", "SubState",
    "-p", "MainPID",
    "-p", "NRestarts",
    "-p", "ExecMainStartTimestamp",
    "--no-pager",
])
counts = run(
    ["docker", "exec", "-i", "teleo-pg", "psql", "-U", "postgres", "-d", "teleo", "-At", "-q"],
    input_text={db_counts_sql()!r},
)
head = run(["git", "rev-parse", "HEAD"])
stamp = run(["cat", "/opt/teleo-eval/.last-deploy-sha"])
status = run(["git", "status", "--short"])
try:
    parsed_counts = json.loads(counts.stdout.strip()) if counts.returncode == 0 and counts.stdout.strip() else None
except Exception:
    parsed_counts = None
payload = {{
    "label": {label!r},
    "remote_repo": {REMOTE_REPO!r},
    "deploy_head": head.stdout.strip(),
    "last_deploy_sha": stamp.stdout.strip(),
    "git_status_short": status.stdout,
    "service": {{
        "returncode": service.returncode,
        "stderr": service.stderr,
        "state": kv(service.stdout),
    }},
    "db_counts": {{
        "returncode": counts.returncode,
        "stderr": counts.stderr,
        "counts": parsed_counts,
        "raw": counts.stdout.strip(),
    }},
}}
print(json.dumps(payload, sort_keys=True))
""".strip()
    return f"cd {shlex.quote(REMOTE_REPO)} && python3 - <<'PY'\n{script}\nPY"


def restart_command() -> str:
    return "systemctl restart leoclean-gateway.service"


def handler_command(args: argparse.Namespace) -> str:
    return (
        f"cd {shlex.quote(REMOTE_REPO)} && "
        "sudo -u teleo -H /home/teleo/.hermes/hermes-agent/venv/bin/python "
        "scripts/run_leo_direct_claim_handler_remote.py "
        f"--turn-timeout {int(args.handler_turn_timeout)} "
        f"--remote-report-path {shlex.quote(args.remote_handler_report)}"
    )


def run_remote_json(args: argparse.Namespace, runner: Runner, remote_command: str, timeout: int) -> dict[str, Any]:
    proc = runner(ssh_command(args, remote_command), "", timeout)
    if proc.returncode != 0:
        raise RuntimeError(redact(proc.stderr.strip() or proc.stdout.strip() or f"remote command exited {proc.returncode}"))
    return parse_last_json_line(proc.stdout)


def is_active_running(readback: dict[str, Any]) -> bool:
    state = ((readback.get("service") or {}).get("state") or {})
    return state.get("ActiveState") == "active" and state.get("SubState") == "running"


def counts(readback: dict[str, Any]) -> dict[str, Any] | None:
    value = ((readback.get("db_counts") or {}).get("counts"))
    if isinstance(value, dict):
        return value
    return None


def count_keys_present(value: dict[str, Any] | None) -> bool:
    return isinstance(value, dict) and all(key in value for key in EXPECTED_COUNTS)


def wait_for_active(args: argparse.Namespace, runner: Runner) -> dict[str, Any]:
    deadline = time.time() + args.active_timeout
    last: dict[str, Any] | None = None
    while time.time() <= deadline:
        last = run_remote_json(args, runner, remote_readback_command("after_restart_poll"), args.timeout)
        if is_active_running(last):
            return last
        time.sleep(args.poll_interval)
    if last is None:
        raise RuntimeError("service did not produce a post-restart readback")
    return last


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def markdown_report(report: dict[str, Any]) -> str:
    before_state = (((report.get("before") or {}).get("service") or {}).get("state") or {})
    after_state = (((report.get("after") or {}).get("service") or {}).get("state") or {})
    handler = report.get("handler") or {}
    lines = [
        "# Leo Restart-Survival Proof",
        "",
        f"Generated UTC: `{report.get('generated_at_utc', '')}`",
        f"Mode: `{report.get('mode', '')}`",
        f"Status: `{report.get('status', '')}`",
        f"Survives restart: `{report.get('survives_restart')}`",
        "",
        "## Safety",
        "",
        f"- Posted to Telegram: `{report.get('posted_to_telegram')}`",
        f"- Production DB apply ran: `{report.get('production_db_apply_ran')}`",
        f"- DB counts changed: `{report.get('db_counts_changed')}`",
        f"- Live profile changed by handler: `{report.get('live_profile_changed_by_handler')}`",
        "",
        "## Service",
        "",
        f"- Before: `ActiveState={before_state.get('ActiveState', '')}`, `SubState={before_state.get('SubState', '')}`, `MainPID={before_state.get('MainPID', '')}`, `NRestarts={before_state.get('NRestarts', '')}`, `ExecMainStartTimestamp={before_state.get('ExecMainStartTimestamp', '')}`",
        f"- After: `ActiveState={after_state.get('ActiveState', '')}`, `SubState={after_state.get('SubState', '')}`, `MainPID={after_state.get('MainPID', '')}`, `NRestarts={after_state.get('NRestarts', '')}`, `ExecMainStartTimestamp={after_state.get('ExecMainStartTimestamp', '')}`",
        "",
        "## Handler Smoke",
        "",
        f"- Handler skipped: `{report.get('handler_skipped')}`",
        f"- Handler pass runtime: `{handler.get('pass_runtime')}`",
        f"- Handler results: `{len(handler.get('results') or [])}`",
        f"- Temp profile removed: `{handler.get('temp_profile_removed')}`",
        "",
        "## Claim Ceiling",
        "",
        "This proves restart-cycle service recovery plus DB-count stability and no-post handler behavior only when `survives_restart=true`. It does not prove Telegram-visible message delivery, production DB apply, or GCP parity.",
        "",
    ]
    return "\n".join(lines)


def collect(args: argparse.Namespace, runner: Runner = subprocess_runner) -> dict[str, Any]:
    report: dict[str, Any] = {
        "generated_at_utc": datetime.now(timezone.utc).isoformat(),
        "mode": "live_vps_leoclean_gateway_restart_survival",
        "ssh_target": args.ssh_target,
        "remote_repo": REMOTE_REPO,
        "remote_handler_report": args.remote_handler_report,
        "posted_to_telegram": False,
        "production_db_apply_ran": False,
        "execute_restart_requested": args.execute_restart,
        "handler_skipped": args.skip_handler,
    }
    try:
        before = run_remote_json(args, runner, remote_readback_command("before_restart"), args.timeout)
        report["before"] = before
        if not args.execute_restart:
            report["status"] = "not_executed_missing_execute_restart"
            report["survives_restart"] = False
            return report

        restart = runner(ssh_command(args, restart_command()), "", args.timeout)
        report["restart_command"] = {
            "returncode": restart.returncode,
            "stdout": redact(restart.stdout),
            "stderr": redact(restart.stderr),
        }
        if restart.returncode != 0:
            report["status"] = "failed_restart_command"
            report["survives_restart"] = False
            return report

        after = wait_for_active(args, runner)
        report["after"] = after
        before_counts = counts(before)
        after_counts = counts(after)
        report["db_counts_changed"] = before_counts != after_counts
        report["db_counts_keys_present"] = count_keys_present(before_counts) and count_keys_present(after_counts)

        handler: dict[str, Any] = {}
        if not args.skip_handler:
            handler = run_remote_json(args, runner, handler_command(args), args.handler_timeout)
        report["handler"] = handler
        report["live_profile_changed_by_handler"] = bool(handler.get("changed_live_profile"))
        report["temp_profile_removed"] = handler.get("temp_profile_removed")

        handler_ok = args.skip_handler or bool(handler.get("pass_runtime"))
        handler_safe = args.skip_handler or (
            handler.get("posted_to_telegram") is False
            and handler.get("mutates_kb_by_harness") is False
            and handler.get("temp_profile_removed") is True
        )
        report["survives_restart"] = bool(
            is_active_running(after)
            and report["db_counts_keys_present"]
            and report["db_counts_changed"] is False
            and handler_ok
            and handler_safe
        )
        report["status"] = "pass" if report["survives_restart"] else "fail"
        return report
    except Exception as exc:
        report["status"] = "error"
        report["survives_restart"] = False
        report["error"] = str(exc)
        return report


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--execute-restart", action="store_true", help="Actually restart leoclean-gateway.service")
    parser.add_argument("--skip-handler", action="store_true", help="Only prove restart and DB-count stability")
    parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
    parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MARKDOWN_OUT)
    parser.add_argument("--ssh-target", default=DEFAULT_SSH_TARGET)
    parser.add_argument("--ssh-key", type=Path, default=DEFAULT_SSH_KEY)
    parser.add_argument("--connect-timeout", type=int, default=10)
    parser.add_argument("--timeout", type=int, default=60)
    parser.add_argument("--active-timeout", type=int, default=90)
    parser.add_argument("--poll-interval", type=float, default=3)
    parser.add_argument("--handler-timeout", type=int, default=2100)
    parser.add_argument("--handler-turn-timeout", type=int, default=300)
    parser.add_argument("--remote-handler-report", default=REMOTE_HANDLER_REPORT)
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    report = collect(args)
    write_json(args.out, report)
    args.markdown_out.parent.mkdir(parents=True, exist_ok=True)
    args.markdown_out.write_text(markdown_report(report), encoding="utf-8")
    print(
        json.dumps(
            {
                "out": str(args.out),
                "markdown_out": str(args.markdown_out),
                "status": report.get("status"),
                "survives_restart": report.get("survives_restart"),
                "db_counts_changed": report.get("db_counts_changed"),
                "posted_to_telegram": report.get("posted_to_telegram"),
                "production_db_apply_ran": report.get("production_db_apply_ran"),
            },
            indent=2,
            sort_keys=True,
        )
    )
    if report.get("status") == "not_executed_missing_execute_restart":
        return 2
    return 0 if report.get("survives_restart") else 1


if __name__ == "__main__":
    raise SystemExit(main())
