"""
Brynq Daemon — Persistent Agent Process Manager.

Keeps agents alive after terminal closes. Spawns, monitors, and restarts
agent processes as detached background jobs.

Usage:
    brynq daemon start                 — start the daemon
    brynq daemon stop                  — stop the daemon and all managed agents
    brynq daemon status                — show daemon and agent health
    brynq daemon launch <name> <task>  — launch a persistent agent
    brynq daemon kill <name>           — stop a specific agent
    brynq daemon list                  — list all managed agents

Storage:
    state/broker/daemon/
        daemon.pid          — daemon process ID
        agents/{name}.json  — per-agent config and status
        logs/{name}.log     — per-agent output logs
"""

import json
import os
import signal
import subprocess
import sys
import time
import uuid
from pathlib import Path
from typing import Optional

from brynq.server import BROKER_DIR, SESSION_NAME, PLATFORM

# ── Directory Layout ─────────────────────────────────────────────────────────

DAEMON_DIR = BROKER_DIR / "daemon"
AGENTS_DIR = DAEMON_DIR / "agents"
LOGS_DIR = DAEMON_DIR / "logs"
PID_FILE = DAEMON_DIR / "daemon.pid"

for _d in (AGENTS_DIR, LOGS_DIR):
    _d.mkdir(parents=True, exist_ok=True)


# ── Agent Record ─────────────────────────────────────────────────────────────

def _agent_path(name: str) -> Path:
    safe = name.replace("/", "_").replace("\\", "_").replace(" ", "_")
    return AGENTS_DIR / f"{safe}.json"


def _read_agent(name: str) -> Optional[dict]:
    path = _agent_path(name)
    try:
        if path.exists():
            return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return None


def _write_agent(name: str, data: dict):
    path = _agent_path(name)
    path.write_text(json.dumps(data, indent=2, default=str), "utf-8")


# ── Process Management ───────────────────────────────────────────────────────

def _is_process_alive(pid: int) -> bool:
    """Check if a process is still running."""
    if pid <= 0:
        return False
    try:
        if sys.platform == "win32":
            import ctypes
            kernel32 = ctypes.windll.kernel32
            handle = kernel32.OpenProcess(0x1000, False, pid)  # PROCESS_QUERY_LIMITED_INFORMATION
            if handle:
                kernel32.CloseHandle(handle)
                return True
            return False
        else:
            os.kill(pid, 0)
            return True
    except (OSError, PermissionError):
        return False


def _spawn_agent_process(
    name: str,
    platform: str,
    task: str,
    working_dir: str = ".",
    model: str = "",
) -> dict:
    """Spawn a detached agent process that survives terminal close."""
    log_path = LOGS_DIR / f"{name}.log"

    if platform in ("claude", "claude-code"):
        cmd = [
            "claude",
            "--dangerously-skip-permissions",
            "--append-system-prompt", f"You are {name}. Your task: {task}. You are connected to the Brynq broker. Use broker tools to coordinate with other agents. Follow Constitution v2.0.",
        ]
    elif platform == "gemini":
        # Launch Gemini via the Brynq CLI bridge — posts arrival and monitors inbox
        cmd = [
            "python", "-m", "brynq.cli", "say",
            f"Agent {name} (Gemini) online. Task: {task}. Monitoring inbox.",
            "--channel", "general",
        ]
    elif platform == "ollama":
        cmd = [
            "python", "-m", "brynq.ollama_adapter",
            "--model", model or "llama3",
            "--name", name,
            "--task", task,
        ]
    else:
        cmd = [
            "python", "-m", "brynq.cli", "post",
            "--message", f"Agent {name} ({platform}) started with task: {task}",
            "--channel", "general",
        ]

    # Spawn detached process
    kwargs = {}
    if sys.platform == "win32":
        # Windows: CREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS
        CREATE_NEW_PROCESS_GROUP = 0x00000200
        DETACHED_PROCESS = 0x00000008
        kwargs["creationflags"] = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
    else:
        kwargs["start_new_session"] = True

    with open(log_path, "a", encoding="utf-8") as log_file:
        log_file.write(f"\n{'='*60}\n")
        log_file.write(f"Agent {name} started at {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
        log_file.write(f"Command: {' '.join(cmd)}\n")
        log_file.write(f"{'='*60}\n\n")
        log_file.flush()

        try:
            proc = subprocess.Popen(
                cmd,
                stdout=log_file,
                stderr=subprocess.STDOUT,
                cwd=working_dir,
                **kwargs,
            )
        except FileNotFoundError:
            return {"success": False, "error": f"Command not found: {cmd[0]}"}

    return {"success": True, "pid": proc.pid}


# ── Agent Lifecycle ──────────────────────────────────────────────────────────

def launch_agent(
    name: str,
    platform: str = "claude",
    task: str = "",
    working_dir: str = ".",
    model: str = "",
    auto_restart: bool = True,
    max_restarts: int = 3,
) -> dict:
    """Launch a persistent agent. Survives terminal close. Auto-restarts on crash."""
    existing = _read_agent(name)
    if existing and existing.get("status") == "running":
        if _is_process_alive(existing.get("pid", 0)):
            return {"success": False, "error": f"Agent '{name}' is already running (PID {existing['pid']})"}
        # Process died but record says running — clean up
        existing["status"] = "crashed"
        _write_agent(name, existing)

    result = _spawn_agent_process(name, platform, task, working_dir, model)
    if not result["success"]:
        return result

    agent = {
        "name": name,
        "platform": platform,
        "task": task,
        "working_dir": working_dir,
        "model": model,
        "pid": result["pid"],
        "status": "running",
        "auto_restart": auto_restart,
        "max_restarts": max_restarts,
        "restart_count": 0,
        "launched_at": time.time(),
        "last_heartbeat": time.time(),
        "launched_by": SESSION_NAME,
    }
    _write_agent(name, agent)

    return {"success": True, "name": name, "pid": result["pid"], "platform": platform}


def kill_agent(name: str) -> dict:
    """Stop a managed agent."""
    agent = _read_agent(name)
    if not agent:
        return {"success": False, "error": f"Agent '{name}' not found"}

    pid = agent.get("pid", 0)
    if pid and _is_process_alive(pid):
        try:
            if sys.platform == "win32":
                subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True)
            else:
                os.kill(pid, signal.SIGTERM)
        except (OSError, PermissionError):
            pass

    agent["status"] = "stopped"
    agent["stopped_at"] = time.time()
    _write_agent(name, agent)

    return {"success": True, "name": name, "pid": pid, "status": "stopped"}


def restart_agent(name: str) -> dict:
    """Restart a managed agent."""
    agent = _read_agent(name)
    if not agent:
        return {"success": False, "error": f"Agent '{name}' not found"}

    # Kill if running
    kill_agent(name)
    time.sleep(1)

    # Relaunch
    result = launch_agent(
        name=agent["name"],
        platform=agent["platform"],
        task=agent.get("task", ""),
        working_dir=agent.get("working_dir", "."),
        model=agent.get("model", ""),
        auto_restart=agent.get("auto_restart", True),
        max_restarts=agent.get("max_restarts", 3),
    )
    if result.get("success"):
        updated = _read_agent(name)
        if updated:
            updated["restart_count"] = agent.get("restart_count", 0) + 1
            _write_agent(name, updated)

    return result


# ── Health Monitoring ────────────────────────────────────────────────────────

def check_health() -> list:
    """Check all managed agents. Auto-restart crashed ones if configured."""
    results = []
    for path in AGENTS_DIR.glob("*.json"):
        try:
            agent = json.loads(path.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue

        name = agent.get("name", path.stem)
        pid = agent.get("pid", 0)
        status = agent.get("status", "unknown")

        if status == "running" and not _is_process_alive(pid):
            # Process died
            agent["status"] = "crashed"
            agent["crashed_at"] = time.time()
            _write_agent(name, agent)

            # Auto-restart if configured
            if agent.get("auto_restart", False):
                restarts = agent.get("restart_count", 0)
                max_restarts = agent.get("max_restarts", 3)
                if restarts < max_restarts:
                    restart_result = restart_agent(name)
                    status = "restarted" if restart_result.get("success") else "restart_failed"
                else:
                    status = "max_restarts_exceeded"
                    agent["status"] = status
                    _write_agent(name, agent)
            else:
                status = "crashed"

        results.append({
            "name": name,
            "platform": agent.get("platform", "unknown"),
            "pid": pid,
            "status": status,
            "task": agent.get("task", "")[:80],
            "restart_count": agent.get("restart_count", 0),
            "launched_at": agent.get("launched_at"),
            "uptime_secs": time.time() - agent.get("launched_at", time.time()) if status == "running" else 0,
        })

    return results


def list_agents() -> list:
    """List all managed agents with their current status."""
    agents = []
    for path in AGENTS_DIR.glob("*.json"):
        try:
            agent = json.loads(path.read_text("utf-8"))
            pid = agent.get("pid", 0)
            alive = _is_process_alive(pid) if pid else False
            agents.append({
                "name": agent.get("name", path.stem),
                "platform": agent.get("platform", "unknown"),
                "pid": pid,
                "alive": alive,
                "status": agent.get("status", "unknown"),
                "task": agent.get("task", "")[:80],
                "restart_count": agent.get("restart_count", 0),
                "auto_restart": agent.get("auto_restart", False),
                "launched_at": agent.get("launched_at"),
                "launched_by": agent.get("launched_by", "unknown"),
            })
        except (json.JSONDecodeError, OSError):
            continue
    return agents


def get_agent_logs(name: str, lines: int = 50) -> str:
    """Get the last N lines of an agent's log file."""
    log_path = LOGS_DIR / f"{name}.log"
    if not log_path.exists():
        return f"No logs found for agent '{name}'"
    try:
        all_lines = log_path.read_text("utf-8").splitlines()
        return "\n".join(all_lines[-lines:])
    except OSError:
        return f"Error reading logs for agent '{name}'"


# ── Daemon Process ───────────────────────────────────────────────────────────

def start_daemon(interval_secs: int = 30):
    """Start the daemon health monitor loop. Runs in background."""
    # Write PID file
    PID_FILE.write_text(str(os.getpid()), "utf-8")

    print(f"Brynq Daemon started (PID {os.getpid()})")
    print(f"Monitoring agents every {interval_secs}s")
    print(f"Agent configs: {AGENTS_DIR}")
    print(f"Logs: {LOGS_DIR}")
    print("Press Ctrl+C to stop.")

    try:
        while True:
            results = check_health()
            running = sum(1 for r in results if r["status"] == "running")
            crashed = sum(1 for r in results if r["status"] in ("crashed", "restart_failed"))
            restarted = sum(1 for r in results if r["status"] == "restarted")

            if restarted or crashed:
                print(f"[{time.strftime('%H:%M:%S')}] Agents: {running} running, {crashed} crashed, {restarted} restarted")

            time.sleep(interval_secs)
    except KeyboardInterrupt:
        print("\nDaemon stopped.")
    finally:
        if PID_FILE.exists():
            PID_FILE.unlink()


def stop_daemon():
    """Stop the daemon and optionally all managed agents."""
    if not PID_FILE.exists():
        return {"success": False, "error": "Daemon is not running (no PID file)"}

    try:
        pid = int(PID_FILE.read_text("utf-8").strip())
    except (ValueError, OSError):
        return {"success": False, "error": "Invalid PID file"}

    if _is_process_alive(pid):
        try:
            if sys.platform == "win32":
                subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True)
            else:
                os.kill(pid, signal.SIGTERM)
        except (OSError, PermissionError):
            pass

    if PID_FILE.exists():
        PID_FILE.unlink()

    return {"success": True, "pid": pid, "status": "stopped"}


def daemon_status() -> dict:
    """Get daemon status."""
    daemon_running = False
    daemon_pid = None

    if PID_FILE.exists():
        try:
            daemon_pid = int(PID_FILE.read_text("utf-8").strip())
            daemon_running = _is_process_alive(daemon_pid)
        except (ValueError, OSError):
            pass

    agents = list_agents()
    running = sum(1 for a in agents if a["alive"])

    return {
        "daemon_running": daemon_running,
        "daemon_pid": daemon_pid,
        "total_agents": len(agents),
        "running_agents": running,
        "crashed_agents": sum(1 for a in agents if a["status"] == "crashed"),
        "agents": agents,
    }


# ── CLI Entry Point ──────────────────────────────────────────────────────────

def cli_main():
    """CLI for daemon management: brynq daemon <command>"""
    import argparse

    parser = argparse.ArgumentParser(description="Brynq Agent Daemon")
    sub = parser.add_subparsers(dest="command")

    sub.add_parser("start", help="Start the health monitor daemon")
    sub.add_parser("stop", help="Stop the daemon")
    sub.add_parser("status", help="Show daemon and agent status")
    sub.add_parser("list", help="List all managed agents")

    p_launch = sub.add_parser("launch", help="Launch a persistent agent")
    p_launch.add_argument("name", help="Agent name")
    p_launch.add_argument("--platform", "-p", default="claude", help="Platform: claude, gemini, ollama")
    p_launch.add_argument("--task", "-t", default="", help="Task description")
    p_launch.add_argument("--model", "-m", default="", help="Model for ollama (e.g. llama3)")
    p_launch.add_argument("--no-restart", action="store_true", help="Disable auto-restart")

    p_kill = sub.add_parser("kill", help="Stop a specific agent")
    p_kill.add_argument("name", help="Agent name to stop")

    p_restart = sub.add_parser("restart", help="Restart an agent")
    p_restart.add_argument("name", help="Agent name to restart")

    p_logs = sub.add_parser("logs", help="View agent logs")
    p_logs.add_argument("name", help="Agent name")
    p_logs.add_argument("--lines", "-n", type=int, default=50)

    p_health = sub.add_parser("health", help="Run health check on all agents")

    args = parser.parse_args()

    if args.command == "start":
        start_daemon()
    elif args.command == "stop":
        result = stop_daemon()
        print(json.dumps(result, indent=2))
    elif args.command == "status":
        result = daemon_status()
        print(f"Daemon: {'RUNNING' if result['daemon_running'] else 'STOPPED'} (PID: {result['daemon_pid']})")
        print(f"Agents: {result['running_agents']}/{result['total_agents']} running")
        for a in result["agents"]:
            alive = "ALIVE" if a["alive"] else "DEAD"
            print(f"  {a['name']} [{a['platform']}] PID:{a['pid']} {alive} restarts:{a['restart_count']} — {a['task']}")
    elif args.command == "list":
        agents = list_agents()
        for a in agents:
            print(f"  {a['name']} [{a['platform']}] PID:{a['pid']} status:{a['status']} restarts:{a['restart_count']}")
    elif args.command == "launch":
        result = launch_agent(
            name=args.name,
            platform=args.platform,
            task=args.task,
            model=args.model,
            auto_restart=not args.no_restart,
        )
        print(json.dumps(result, indent=2))
    elif args.command == "kill":
        result = kill_agent(args.name)
        print(json.dumps(result, indent=2))
    elif args.command == "restart":
        result = restart_agent(args.name)
        print(json.dumps(result, indent=2))
    elif args.command == "logs":
        print(get_agent_logs(args.name, args.lines))
    elif args.command == "health":
        results = check_health()
        for r in results:
            print(f"  {r['name']} [{r['platform']}] — {r['status']} (PID:{r['pid']}, uptime:{r['uptime_secs']:.0f}s)")
    else:
        parser.print_help()


if __name__ == "__main__":
    cli_main()
