#!/usr/bin/env python3
"""
fleet-dashboard.py -- Timmy Foundation Fleet Status Dashboard.

One-page terminal dashboard showing:
  1. Gitea: open PRs, open issues, recent merges
  2. VPS health: SSH reachability, service status, disk usage
  3. Cron jobs: scheduled jobs, last run status

Usage:
    python3 scripts/fleet-dashboard.py
    python3 scripts/fleet-dashboard.py --json   # machine-readable output
"""

from __future__ import annotations

import json
import os
import socket
import subprocess
import sys
import time
import urllib.request
import urllib.error
from datetime import datetime, timezone, timedelta
from pathlib import Path

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

GITEA_BASE = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
GITEA_API = f"{GITEA_BASE}/api/v1"
GITEA_ORG = "Timmy_Foundation"

# Key repos to check for PRs/issues
REPOS = [
    "timmy-config",
    "the-nexus",
    "hermes-agent",
    "the-forge",
    "timmy-sandbox",
]

# VPS fleet
VPS_HOSTS = {
    "ezra": {
        "ip": "143.198.27.163",
        "ssh_user": "root",
        "services": ["nginx", "gitea", "docker"],
    },
    "allegro": {
        "ip": "167.99.126.228",
        "ssh_user": "root",
        "services": ["hermes-agent"],
    },
    "bezalel": {
        "ip": "159.203.146.185",
        "ssh_user": "root",
        "services": ["hermes-agent", "evennia"],
    },
}

CRON_JOBS_FILE = Path(__file__).parent.parent / "cron" / "jobs.json"

# ---------------------------------------------------------------------------
# Gitea helpers
# ---------------------------------------------------------------------------

def _gitea_token() -> str:
    for p in [
        Path.home() / ".hermes" / "gitea_token",
        Path.home() / ".hermes" / "gitea_token_vps",
        Path.home() / ".config" / "gitea" / "token",
    ]:
        if p.exists():
            return p.read_text().strip()
    return ""


def _gitea_get(path: str, params: dict | None = None) -> list | dict:
    url = f"{GITEA_API}{path}"
    if params:
        qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
        if qs:
            url += f"?{qs}"
    req = urllib.request.Request(url)
    token = _gitea_token()
    if token:
        req.add_header("Authorization", f"token {token}")
    req.add_header("Accept", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            return json.loads(resp.read())
    except Exception as e:
        return {"error": str(e)}


def check_gitea_health() -> dict:
    """Ping Gitea and collect PR/issue stats."""
    result = {"reachable": False, "version": "", "repos": {}, "totals": {}}

    # Ping
    data = _gitea_get("/version")
    if isinstance(data, dict) and "error" not in data:
        result["reachable"] = True
        result["version"] = data.get("version", "unknown")
    elif isinstance(data, dict) and "error" in data:
        return result

    total_open_prs = 0
    total_open_issues = 0
    total_recent_merges = 0
    cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ")

    for repo in REPOS:
        repo_path = f"/repos/{GITEA_ORG}/{repo}"
        repo_info = {"prs": [], "issues": [], "recent_merges": 0}

        # Open PRs
        prs = _gitea_get(f"{repo_path}/pulls", {"state": "open", "limit": "10", "sort": "newest"})
        if isinstance(prs, list):
            for pr in prs:
                repo_info["prs"].append({
                    "number": pr.get("number"),
                    "title": pr.get("title", "")[:60],
                    "user": pr.get("user", {}).get("login", "unknown"),
                    "created": pr.get("created_at", "")[:10],
                })
            total_open_prs += len(prs)

        # Open issues (excluding PRs)
        issues = _gitea_get(f"{repo_path}/issues", {
            "state": "open", "type": "issues", "limit": "10", "sort": "newest"
        })
        if isinstance(issues, list):
            for iss in issues:
                repo_info["issues"].append({
                    "number": iss.get("number"),
                    "title": iss.get("title", "")[:60],
                    "user": iss.get("user", {}).get("login", "unknown"),
                    "created": iss.get("created_at", "")[:10],
                })
            total_open_issues += len(issues)

        # Recent merges (closed PRs)
        merged = _gitea_get(f"{repo_path}/pulls", {"state": "closed", "limit": "20", "sort": "newest"})
        if isinstance(merged, list):
            recent = [p for p in merged if p.get("merged") and p.get("closed_at", "") >= cutoff]
            repo_info["recent_merges"] = len(recent)
            total_recent_merges += len(recent)

        result["repos"][repo] = repo_info

    result["totals"] = {
        "open_prs": total_open_prs,
        "open_issues": total_open_issues,
        "recent_merges_7d": total_recent_merges,
    }
    return result


# ---------------------------------------------------------------------------
# VPS health helpers
# ---------------------------------------------------------------------------

def check_ssh(ip: str, timeout: int = 5) -> bool:
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((ip, 22))
        sock.close()
        return result == 0
    except Exception:
        return False


def check_service(ip: str, user: str, service: str) -> str:
    """Check if a systemd service is active on remote host."""
    cmd = f"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 {user}@{ip} 'systemctl is-active {service} 2>/dev/null || echo inactive'"
    try:
        proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
        return proc.stdout.strip() or "unknown"
    except subprocess.TimeoutExpired:
        return "timeout"
    except Exception:
        return "error"


def check_disk(ip: str, user: str) -> dict:
    cmd = f"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 {user}@{ip} 'df -h / | tail -1'"
    try:
        proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
        if proc.returncode == 0:
            parts = proc.stdout.strip().split()
            if len(parts) >= 5:
                return {"total": parts[1], "used": parts[2], "avail": parts[3], "pct": parts[4]}
    except Exception:
        pass
    return {"total": "?", "used": "?", "avail": "?", "pct": "?"}


def check_vps_health() -> dict:
    result = {}
    for name, cfg in VPS_HOSTS.items():
        ip = cfg["ip"]
        ssh_up = check_ssh(ip)
        entry = {"ip": ip, "ssh": ssh_up, "services": {}, "disk": {}}
        if ssh_up:
            for svc in cfg.get("services", []):
                entry["services"][svc] = check_service(ip, cfg["ssh_user"], svc)
            entry["disk"] = check_disk(ip, cfg["ssh_user"])
        result[name] = entry
    return result


# ---------------------------------------------------------------------------
# Cron job status
# ---------------------------------------------------------------------------

def check_cron_jobs() -> list[dict]:
    jobs = []
    if not CRON_JOBS_FILE.exists():
        return [{"name": "jobs.json", "status": "FILE NOT FOUND"}]
    try:
        data = json.loads(CRON_JOBS_FILE.read_text())
        for job in data.get("jobs", []):
            jobs.append({
                "name": job.get("name", "unnamed"),
                "schedule": job.get("schedule_display", job.get("schedule", {}).get("display", "?")),
                "enabled": job.get("enabled", False),
                "state": job.get("state", "unknown"),
                "completed": job.get("repeat", {}).get("completed", 0),
                "last_status": job.get("last_status") or "never run",
                "last_error": job.get("last_error"),
            })
    except Exception as e:
        jobs.append({"name": "jobs.json", "status": f"PARSE ERROR: {e}"})
    return jobs


# ---------------------------------------------------------------------------
# Terminal rendering
# ---------------------------------------------------------------------------

BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
RESET = "\033[0m"


def _ok(val: bool) -> str:
    return f"{GREEN}UP{RESET}" if val else f"{RED}DOWN{RESET}"


def _svc_icon(status: str) -> str:
    s = status.lower().strip()
    if s in ("active", "running"):
        return f"{GREEN}active{RESET}"
    elif s in ("inactive", "dead", "failed"):
        return f"{RED}{s}{RESET}"
    elif s == "timeout":
        return f"{YELLOW}timeout{RESET}"
    else:
        return f"{YELLOW}{s}{RESET}"


def render_dashboard(gitea: dict, vps: dict, cron: list[dict]) -> str:
    lines = []
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
    lines.append("")
    lines.append(f"{BOLD}{'=' * 72}{RESET}")
    lines.append(f"{BOLD}  TIMMY FOUNDATION -- FLEET STATUS DASHBOARD{RESET}")
    lines.append(f"{DIM}  Generated: {now}{RESET}")
    lines.append(f"{BOLD}{'=' * 72}{RESET}")

    # ── Section 1: Gitea ──────────────────────────────────────────────────
    lines.append("")
    lines.append(f"{BOLD}{CYAN}  [1] GITEA{RESET}")
    lines.append(f"  {'-' * 68}")
    if gitea.get("reachable"):
        lines.append(f"  Status: {GREEN}REACHABLE{RESET}  (version {gitea.get('version', '?')})")
        t = gitea.get("totals", {})
        lines.append(f"  Totals: {t.get('open_prs', 0)} open PRs  |  {t.get('open_issues', 0)} open issues  |  {t.get('recent_merges_7d', 0)} merges (7d)")
        lines.append("")
        for repo_name, repo in gitea.get("repos", {}).items():
            prs = repo.get("prs", [])
            issues = repo.get("issues", [])
            merges = repo.get("recent_merges", 0)
            lines.append(f"  {BOLD}{repo_name}{RESET}  ({len(prs)} PRs, {len(issues)} issues, {merges} merges/7d)")
            for pr in prs[:5]:
                lines.append(f"    PR #{pr['number']:>4}  {pr['title'][:50]:<50}  {DIM}{pr['user']}{RESET}  {pr['created']}")
            for iss in issues[:3]:
                lines.append(f"    IS #{iss['number']:>4}  {iss['title'][:50]:<50}  {DIM}{iss['user']}{RESET}  {iss['created']}")
    else:
        lines.append(f"  Status: {RED}UNREACHABLE{RESET}")

    # ── Section 2: VPS Health ─────────────────────────────────────────────
    lines.append("")
    lines.append(f"{BOLD}{CYAN}  [2] VPS HEALTH{RESET}")
    lines.append(f"  {'-' * 68}")
    lines.append(f"  {'Host':<12} {'IP':<18} {'SSH':<8} {'Disk':<12} {'Services'}")
    lines.append(f"  {'-' * 12} {'-' * 17} {'-' * 7} {'-' * 11} {'-' * 30}")
    for name, info in vps.items():
        ssh_str = _ok(info["ssh"])
        disk = info.get("disk", {})
        disk_str = disk.get("pct", "?")
        if disk_str != "?":
            pct_val = int(disk_str.rstrip("%"))
            if pct_val >= 90:
                disk_str = f"{RED}{disk_str}{RESET}"
            elif pct_val >= 75:
                disk_str = f"{YELLOW}{disk_str}{RESET}"
            else:
                disk_str = f"{GREEN}{disk_str}{RESET}"
        svc_parts = []
        for svc, status in info.get("services", {}).items():
            svc_parts.append(f"{svc}:{_svc_icon(status)}")
        svc_str = "  ".join(svc_parts) if svc_parts else f"{DIM}n/a{RESET}"
        lines.append(f"  {name:<12} {info['ip']:<18} {ssh_str:<18} {disk_str:<22} {svc_str}")

    # ── Section 3: Cron Jobs ──────────────────────────────────────────────
    lines.append("")
    lines.append(f"{BOLD}{CYAN}  [3] CRON JOBS{RESET}")
    lines.append(f"  {'-' * 68}")
    lines.append(f"  {'Name':<28} {'Schedule':<16} {'State':<12} {'Last':<12} {'Runs'}")
    lines.append(f"  {'-' * 27} {'-' * 15} {'-' * 11} {'-' * 11} {'-' * 5}")
    for job in cron:
        name = job.get("name", "?")[:27]
        sched = job.get("schedule", "?")[:15]
        state = job.get("state", "?")
        if state == "scheduled":
            state_str = f"{GREEN}{state}{RESET}"
        elif state == "paused":
            state_str = f"{YELLOW}{state}{RESET}"
        else:
            state_str = state
        last = job.get("last_status", "never")[:11]
        if last == "ok":
            last_str = f"{GREEN}{last}{RESET}"
        elif last in ("error", "never run"):
            last_str = f"{RED}{last}{RESET}"
        else:
            last_str = last
        runs = job.get("completed", 0)
        enabled = job.get("enabled", False)
        marker = " " if enabled else f"{DIM}(disabled){RESET}"
        lines.append(f"  {name:<28} {sched:<16} {state_str:<22} {last_str:<22} {runs}  {marker}")

    # ── Footer ────────────────────────────────────────────────────────────
    lines.append("")
    lines.append(f"{BOLD}{'=' * 72}{RESET}")
    lines.append(f"{DIM}  python3 scripts/fleet-dashboard.py  |  timmy-config{RESET}")
    lines.append(f"{BOLD}{'=' * 72}{RESET}")
    lines.append("")

    return "\n".join(lines)


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

def main():
    json_mode = "--json" in sys.argv

    if not json_mode:
        print(f"\n  {DIM}Collecting fleet data...{RESET}\n", file=sys.stderr)

    gitea = check_gitea_health()
    vps = check_vps_health()
    cron = check_cron_jobs()

    if json_mode:
        output = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "gitea": gitea,
            "vps": vps,
            "cron": cron,
        }
        print(json.dumps(output, indent=2))
    else:
        print(render_dashboard(gitea, vps, cron))


if __name__ == "__main__":
    main()
