#!/usr/bin/env python3
"""ClaudeTimeline API server lifecycle management.

Usage:
    uv run scripts/serve start    # Start server in background, write PID file
    uv run scripts/serve stop     # Stop server using PID file
    uv run scripts/serve status   # Check if server is running
    uv run scripts/serve restart  # Stop + start
    uv run scripts/serve fg       # Start in foreground with --reload (dev mode)
    uv run scripts/serve          # No subcommand = foreground (same as fg)
"""

import os
import signal
import subprocess
import sys
import time
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = PROJECT_ROOT / "data"
PID_FILE = DATA_DIR / "server.pid"
LOG_FILE = DATA_DIR / "server.log"
HOST = "127.0.0.1"
PORT = 8000
APP_MODULE = "claude_timeline.server.app:app"


def _read_pid() -> int | None:
    """Read PID from the PID file. Returns None if file doesn't exist or is invalid."""
    try:
        return int(PID_FILE.read_text().strip())
    except (FileNotFoundError, ValueError):
        return None


def _is_alive(pid: int) -> bool:
    """Check if a process with the given PID is alive."""
    try:
        os.kill(pid, 0)  # Signal 0 = existence check, no actual signal sent
        return True
    except OSError:
        return False


def _clean_stale_pid() -> None:
    """Remove PID file if the recorded process is no longer alive."""
    pid = _read_pid()
    if pid is not None and not _is_alive(pid):
        PID_FILE.unlink(missing_ok=True)


def _ensure_frontend_built() -> None:
    """Build the frontend if dist/index.html doesn't exist."""
    dist_index = PROJECT_ROOT / "frontend" / "dist" / "index.html"
    if not dist_index.exists():
        print("Building frontend...", file=sys.stderr)
        subprocess.run(
            ["npm", "run", "build"],
            cwd=PROJECT_ROOT / "frontend",
            check=True,
        )


def cmd_start() -> None:
    """Start the server as a detached background process."""
    _ensure_frontend_built()
    _clean_stale_pid()

    pid = _read_pid()
    if pid is not None and _is_alive(pid):
        print(f"Server is already running (PID {pid}).")
        print(f"  http://{HOST}:{PORT}")
        return

    DATA_DIR.mkdir(parents=True, exist_ok=True)

    # Hand off log management to the in-process RotatingFileHandler installed by
    # observability.install() when CLAUDE_TIMELINE_LOG_FILE is set. We still
    # send Popen's stdout/stderr to the same file in append mode so any output
    # uvicorn emits BEFORE the FastAPI app is imported (very early bootstrap,
    # tracebacks during import) is captured. Once the app is up, the runtime
    # rotating handler takes over and rotates at LOG_MAX_BYTES (10 MB, 5
    # backups). Logs from both paths interleave fine since the handler uses
    # the standard append-and-rename rotation strategy.
    env = {**os.environ, "CLAUDE_TIMELINE_LOG_FILE": str(LOG_FILE)}
    log_fd = open(LOG_FILE, "a")  # noqa: SIM115 -- need raw fd for subprocess
    proc = subprocess.Popen(
        [
            sys.executable, "-m", "uvicorn",
            APP_MODULE,
            "--host", HOST,
            "--port", str(PORT),
        ],
        stdout=log_fd,
        stderr=log_fd,
        # Detach from the parent process so it survives after this script exits
        start_new_session=True,
        cwd=PROJECT_ROOT,
        env=env,
    )
    log_fd.close()

    PID_FILE.write_text(str(proc.pid))
    print(f"Server started (PID {proc.pid}).")
    print(f"  http://{HOST}:{PORT}")
    print(f"  Log: {LOG_FILE}")


def cmd_stop() -> None:
    """Stop the server using the PID file. SIGTERM first, SIGKILL after 5s."""
    _clean_stale_pid()

    pid = _read_pid()
    if pid is None:
        print("Server is not running.")
        return

    print(f"Stopping server (PID {pid})...")
    try:
        os.kill(pid, signal.SIGTERM)
    except OSError:
        # Process already gone
        PID_FILE.unlink(missing_ok=True)
        print("Server stopped (process already exited).")
        return

    # Wait up to 5 seconds for graceful shutdown
    for _ in range(50):
        if not _is_alive(pid):
            break
        time.sleep(0.1)
    else:
        # Still alive after 5s -- force kill
        print(f"Process {pid} did not exit gracefully, sending SIGKILL...")
        try:
            os.kill(pid, signal.SIGKILL)
            time.sleep(0.2)
        except OSError:
            pass

    PID_FILE.unlink(missing_ok=True)
    print("Server stopped.")


def cmd_status() -> None:
    """Print whether the server is currently running."""
    _clean_stale_pid()

    pid = _read_pid()
    if pid is not None and _is_alive(pid):
        print(f"Server is running (PID {pid}).")
        print(f"  http://{HOST}:{PORT}")
    else:
        print("Server is not running.")


def cmd_restart() -> None:
    """Stop the server if running, then start it."""
    cmd_stop()
    cmd_start()


def cmd_foreground() -> None:
    """Run the server in the foreground with --reload for development."""
    _ensure_frontend_built()
    import uvicorn
    print(f"Starting server in foreground (dev mode with --reload)...")
    print(f"  http://{HOST}:{PORT}")
    uvicorn.run(APP_MODULE, host=HOST, port=PORT, reload=True)


COMMANDS = {
    "start": cmd_start,
    "stop": cmd_stop,
    "status": cmd_status,
    "restart": cmd_restart,
    "fg": cmd_foreground,
}


def main() -> None:
    subcommand = sys.argv[1] if len(sys.argv) > 1 else None

    if subcommand in ("--help", "-h"):
        print(__doc__)
        return

    if subcommand is None or subcommand == "fg":
        cmd_foreground()
    elif subcommand in COMMANDS:
        COMMANDS[subcommand]()
    else:
        print(f"Unknown subcommand: {subcommand}")
        print(f"Usage: {sys.argv[0]} [start|stop|status|restart|fg]")
        sys.exit(1)


if __name__ == "__main__":
    main()
