"""linear-canal daemon — Always-on background MCP server.

Runs the Linear Canal MCP server in the background so AI agents can connect
without any manual `linear-canal serve` step.

Once installed the MCP endpoint is always available at:
    http://127.0.0.1:3141/mcp

Usage
-----
    linear-canal daemon start          Start daemon now
    linear-canal daemon stop           Stop daemon
    linear-canal daemon restart        Restart daemon
    linear-canal daemon status         Show whether daemon is running
    linear-canal daemon install        Install OS autostart (launchd / systemd / Task Scheduler)
    linear-canal daemon uninstall      Remove OS autostart
    linear-canal daemon logs           Tail daemon logs

.mcp.json snippet (add to your project after running `daemon install`)
----------------------------------------------------------------------
    {
      "mcpServers": {
        "linear-canal": {
          "url": "http://127.0.0.1:3141/mcp"
        }
      }
    }
"""
from __future__ import annotations

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

import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

# ── Constants ─────────────────────────────────────────────────────────────────

_LC_HOME       = Path.home() / ".linear-canal"
_DAEMON_DIR    = _LC_HOME / "daemon"
_PID_FILE      = _DAEMON_DIR / "daemon.pid"
_LOG_FILE      = _DAEMON_DIR / "daemon.log"
_PORT_FILE     = _DAEMON_DIR / "daemon.port"
_DEFAULT_PORT  = 3141
_DEFAULT_HOST  = "127.0.0.1"   # localhost only — never exposed externally

_LANCEDB_PATH  = str(_LC_HOME / "data" / "lancedb_global_store")

# OS autostart identifiers
_LAUNCH_AGENT_LABEL = "dev.linearcanal.mcp"
_SYSTEMD_UNIT       = "linear-canal-mcp.service"


# ── Click group ───────────────────────────────────────────────────────────────

@click.group("daemon")
def daemon_cmd() -> None:
    """Manage the always-on background MCP server."""


# ── start ─────────────────────────────────────────────────────────────────────

@daemon_cmd.command("start")
@click.option("--port", default=_DEFAULT_PORT, show_default=True, help="Port to listen on.")
@click.option("--force", is_flag=True, help="Kill any existing daemon and restart.")
def daemon_start(port: int, force: bool) -> None:
    """Start the background MCP server."""
    if _is_running():
        existing_port = _read_port()
        if force:
            console.print("[yellow]Stopping existing daemon...[/yellow]")
            _do_stop()
            time.sleep(1)
        else:
            console.print(
                f"[green]Daemon already running[/green] on port [bold]{existing_port}[/bold]\n"
                f"  Endpoint: [cyan]http://{_DEFAULT_HOST}:{existing_port}/mcp[/cyan]\n"
                "  Use [bold]--force[/bold] to restart."
            )
            return

    _DAEMON_DIR.mkdir(parents=True, exist_ok=True)
    _do_start(port)


@daemon_cmd.command("stop")
def daemon_stop() -> None:
    """Stop the background MCP server."""
    if not _is_running():
        console.print("[yellow]Daemon is not running.[/yellow]")
        return
    _do_stop()
    console.print("[green]✓[/green] Daemon stopped.")


@daemon_cmd.command("restart")
@click.option("--port", default=None, type=int, help="Port (defaults to current or 3141).")
def daemon_restart(port: Optional[int]) -> None:
    """Restart the background MCP server."""
    current_port = _read_port() or _DEFAULT_PORT
    use_port     = port or current_port
    if _is_running():
        _do_stop()
        time.sleep(1)
    _DAEMON_DIR.mkdir(parents=True, exist_ok=True)
    _do_start(use_port)


@daemon_cmd.command("status")
def daemon_status() -> None:
    """Show daemon status and endpoint info."""
    running = _is_running()
    port    = _read_port() or _DEFAULT_PORT

    t = Table(show_header=False, box=None, padding=(0, 2))
    t.add_column("key",   style="grey58", no_wrap=True)
    t.add_column("value", style="white")

    status_text = "[bold green]Running[/bold green]" if running else "[bold red]Stopped[/bold red]"
    t.add_row("Status",   status_text)

    if running:
        pid = _read_pid()
        t.add_row("PID",      str(pid) if pid else "—")
        t.add_row("Port",     str(port))
        t.add_row("Endpoint", f"http://{_DEFAULT_HOST}:{port}/mcp")
        t.add_row("Log",      str(_LOG_FILE))

    console.print(Panel(t, title="[bold orange1]Linear Canal MCP Daemon[/bold orange1]",
                        border_style="green" if running else "red"))

    if running:
        # Quick health check
        try:
            import urllib.request
            with urllib.request.urlopen(
                f"http://{_DEFAULT_HOST}:{port}/mcp/health", timeout=2
            ) as r:
                data = json.loads(r.read())
                console.print(f"[dim]Health check:[/dim] [green]{data}[/green]")
        except Exception as exc:
            console.print(f"[yellow]Health check failed:[/yellow] {exc}")


@daemon_cmd.command("logs")
@click.option("--lines", "-n", default=50, show_default=True, help="Lines to show.")
@click.option("--follow", "-f", is_flag=True, help="Follow log output (tail -f).")
def daemon_logs(lines: int, follow: bool) -> None:
    """Show daemon logs."""
    if not _LOG_FILE.exists():
        console.print("[yellow]No log file found. Has the daemon ever started?[/yellow]")
        return

    if follow:
        if sys.platform == "win32":
            subprocess.run(["powershell", "-Command", f"Get-Content -Wait '{_LOG_FILE}'"])
        else:
            subprocess.run(["tail", "-f", "-n", str(lines), str(_LOG_FILE)])
    else:
        text = _LOG_FILE.read_text(encoding="utf-8", errors="replace")
        tail = "\n".join(text.splitlines()[-lines:])
        console.print(tail)


@daemon_cmd.command("install")
@click.option("--port", default=_DEFAULT_PORT, show_default=True)
def daemon_install(port: int) -> None:
    """Install OS autostart so the daemon starts on login."""
    _DAEMON_DIR.mkdir(parents=True, exist_ok=True)
    _do_install_autostart(port)


@daemon_cmd.command("uninstall")
def daemon_uninstall() -> None:
    """Remove OS autostart."""
    _do_uninstall_autostart()
    if _is_running():
        _do_stop()
        console.print("[green]✓[/green] Daemon stopped.")


# ── Internal helpers ──────────────────────────────────────────────────────────

def _read_pid() -> Optional[int]:
    try:
        return int(_PID_FILE.read_text().strip())
    except Exception:
        return None


def _read_port() -> Optional[int]:
    try:
        return int(_PORT_FILE.read_text().strip())
    except Exception:
        return None


def _is_running() -> bool:
    pid = _read_pid()
    if pid is None:
        return False
    try:
        if sys.platform == "win32":
            import ctypes
            handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)  # type: ignore[attr-defined]
            if handle:
                ctypes.windll.kernel32.CloseHandle(handle)              # type: ignore[attr-defined]
                return True
            return False
        else:
            os.kill(pid, 0)   # signal 0 = just check if process exists
            return True
    except (OSError, ProcessLookupError):
        return False


def _do_start(port: int) -> None:
    """Start the daemon process and write PID file."""
    # Find the current Python executable (same env that has linear-canal installed)
    python = _find_python()

    # The daemon subprocess runs mcp_http_server.run_server() directly
    cmd = [
        python, "-c",
        f"from linear_canal.mcp_http_server import run_server; "
        f"run_server({port!r}, {_LANCEDB_PATH!r}, None, 'global')"
    ]

    log_handle = _LOG_FILE.open("a", encoding="utf-8")

    if sys.platform == "win32":
        import ctypes
        DETACHED_PROCESS    = 0x00000008
        CREATE_NO_WINDOW    = 0x08000000
        proc = subprocess.Popen(
            cmd,
            stdout=log_handle,
            stderr=log_handle,
            stdin=subprocess.DEVNULL,
            creationflags=DETACHED_PROCESS | CREATE_NO_WINDOW,
            close_fds=True,
        )
    else:
        proc = subprocess.Popen(
            cmd,
            stdout=log_handle,
            stderr=log_handle,
            stdin=subprocess.DEVNULL,
            start_new_session=True,   # detach from terminal
            close_fds=True,
        )

    log_handle.close()

    # Wait briefly to confirm it started
    time.sleep(1.2)
    if proc.poll() is not None:
        console.print(f"[red]✗ Daemon failed to start. Check logs: {_LOG_FILE}[/red]")
        return

    _PID_FILE.write_text(str(proc.pid))
    _PORT_FILE.write_text(str(port))

    console.print(
        f"[green]✓[/green] MCP daemon started  "
        f"[dim]pid={proc.pid}[/dim]\n\n"
        f"  Endpoint → [bold cyan]http://{_DEFAULT_HOST}:{port}/mcp[/bold cyan]\n\n"
        f"  Add to your project's [bold].mcp.json[/bold]:\n"
        f'  [dim]{{"mcpServers":{{"linear-canal":{{"url":"http://{_DEFAULT_HOST}:{port}/mcp"}}}}}}'
        f"[/dim]"
    )


def _do_stop() -> None:
    """Stop the running daemon."""
    pid = _read_pid()
    if pid is None:
        return
    try:
        if sys.platform == "win32":
            subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True)
        else:
            os.kill(pid, signal.SIGTERM)
            # Give it 3s to exit gracefully, then SIGKILL
            for _ in range(30):
                time.sleep(0.1)
                try:
                    os.kill(pid, 0)
                except ProcessLookupError:
                    break
            else:
                os.kill(pid, signal.SIGKILL)
    except ProcessLookupError:
        pass
    except Exception as exc:
        console.print(f"[yellow]Warning stopping process: {exc}[/yellow]")
    finally:
        _PID_FILE.unlink(missing_ok=True)


def _find_python() -> str:
    """Return the Python executable that has linear-canal installed."""
    # Prefer the same interpreter running right now
    return sys.executable


# ── OS autostart ──────────────────────────────────────────────────────────────

def _do_install_autostart(port: int) -> None:
    platform = sys.platform
    python   = _find_python()
    lancedb  = _LANCEDB_PATH

    server_cmd = (
        f"from linear_canal.mcp_http_server import run_server; "
        f"run_server({port!r}, {lancedb!r}, None, 'global')"
    )

    if platform == "darwin":
        _install_launchd(python, server_cmd, port)
    elif platform.startswith("linux"):
        _install_systemd(python, server_cmd, port)
    elif platform == "win32":
        _install_task_scheduler(python, server_cmd, port)
    else:
        console.print(f"[yellow]Unsupported platform for autostart: {platform}[/yellow]")
        return

    # Start immediately after installing
    if not _is_running():
        _do_start(port)


def _install_launchd(python: str, server_cmd: str, port: int) -> None:
    """Install a launchd LaunchAgent plist (macOS)."""
    agents_dir = Path.home() / "Library" / "LaunchAgents"
    agents_dir.mkdir(parents=True, exist_ok=True)
    plist_path = agents_dir / f"{_LAUNCH_AGENT_LABEL}.plist"

    log_out = str(_DAEMON_DIR / "daemon.log")
    log_err = str(_DAEMON_DIR / "daemon.err.log")
    _DAEMON_DIR.mkdir(parents=True, exist_ok=True)

    plist = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>{_LAUNCH_AGENT_LABEL}</string>

    <key>ProgramArguments</key>
    <array>
        <string>{python}</string>
        <string>-c</string>
        <string>{server_cmd}</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>ThrottleInterval</key>
    <integer>10</integer>

    <key>StandardOutPath</key>
    <string>{log_out}</string>

    <key>StandardErrorPath</key>
    <string>{log_err}</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>LANCEDB_PATH</key>
        <string>{_LANCEDB_PATH}</string>
        <key>LC_MCP_PORT</key>
        <string>{port}</string>
    </dict>
</dict>
</plist>
"""
    plist_path.write_text(plist, encoding="utf-8")
    console.print(f"  [green]✓[/green] LaunchAgent plist → [dim]{plist_path}[/dim]")

    # Load immediately
    result = subprocess.run(
        ["launchctl", "load", "-w", str(plist_path)],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        console.print("  [green]✓[/green] launchctl loaded — daemon will start now and on every login.")
    else:
        console.print(f"  [yellow]launchctl load failed:[/yellow] {result.stderr.strip()}")
        console.print(f"  [dim]You can load it manually: launchctl load -w {plist_path}[/dim]")


def _install_systemd(python: str, server_cmd: str, port: int) -> None:
    """Install a systemd user service (Linux)."""
    systemd_dir = Path.home() / ".config" / "systemd" / "user"
    systemd_dir.mkdir(parents=True, exist_ok=True)
    unit_path = systemd_dir / _SYSTEMD_UNIT

    unit = f"""[Unit]
Description=Linear Canal MCP Server
After=network.target

[Service]
Type=simple
ExecStart={python} -c "{server_cmd}"
Environment=LANCEDB_PATH={_LANCEDB_PATH}
Environment=LC_MCP_PORT={port}
Restart=on-failure
RestartSec=10
StandardOutput=append:{_LOG_FILE}
StandardError=append:{_LOG_FILE}

[Install]
WantedBy=default.target
"""
    unit_path.write_text(unit, encoding="utf-8")
    console.print(f"  [green]✓[/green] systemd unit → [dim]{unit_path}[/dim]")

    # Reload and enable
    for cmd, label in [
        (["systemctl", "--user", "daemon-reload"], "daemon-reload"),
        (["systemctl", "--user", "enable", "--now", _SYSTEMD_UNIT], "enable + start"),
    ]:
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode == 0:
            console.print(f"  [green]✓[/green] systemctl {label}")
        else:
            err = result.stderr.strip()
            console.print(f"  [yellow]systemctl {label} failed:[/yellow] {err}")

    console.print(
        "  [dim]Tip: run [bold]loginctl enable-linger[/bold] so the service "
        "starts even without an active session.[/dim]"
    )


def _install_task_scheduler(python: str, server_cmd: str, port: int) -> None:
    """Register a Windows Task Scheduler task (Windows)."""
    task_name = "LinearCanalMCP"
    log_path  = str(_LOG_FILE)

    # Use schtasks to create a task that runs at logon
    xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Triggers>
    <LogonTrigger><Enabled>true</Enabled></LogonTrigger>
  </Triggers>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
    <RestartOnFailure>
      <Interval>PT1M</Interval>
      <Count>999</Count>
    </RestartOnFailure>
  </Settings>
  <Actions>
    <Exec>
      <Command>{python}</Command>
      <Arguments>-c "{server_cmd}"</Arguments>
    </Exec>
  </Actions>
</Task>
"""
    # Write temp XML and import it
    import tempfile
    with tempfile.NamedTemporaryFile(suffix=".xml", mode="w", encoding="utf-16",
                                    delete=False) as f:
        f.write(xml)
        tmp_xml = f.name

    result = subprocess.run(
        ["schtasks", "/Create", "/F", "/TN", task_name, "/XML", tmp_xml],
        capture_output=True, text=True
    )
    Path(tmp_xml).unlink(missing_ok=True)

    if result.returncode == 0:
        console.print(f"  [green]✓[/green] Task Scheduler task '[bold]{task_name}[/bold]' created.")
        # Run it immediately
        subprocess.run(["schtasks", "/Run", "/TN", task_name], capture_output=True)
        console.print(f"  [green]✓[/green] Task started — will auto-start on every login.")
    else:
        console.print(f"  [yellow]schtasks failed:[/yellow] {result.stderr.strip()}")
        console.print(
            f"  [dim]Start manually: python -c \"{server_cmd}\"[/dim]"
        )


def _do_uninstall_autostart() -> None:
    platform = sys.platform
    if platform == "darwin":
        plist = Path.home() / "Library" / "LaunchAgents" / f"{_LAUNCH_AGENT_LABEL}.plist"
        if plist.exists():
            subprocess.run(["launchctl", "unload", "-w", str(plist)], capture_output=True)
            plist.unlink()
            console.print(f"[green]✓[/green] LaunchAgent removed.")
        else:
            console.print("[yellow]No LaunchAgent found.[/yellow]")

    elif platform.startswith("linux"):
        result = subprocess.run(
            ["systemctl", "--user", "disable", "--now", _SYSTEMD_UNIT],
            capture_output=True, text=True
        )
        unit = Path.home() / ".config" / "systemd" / "user" / _SYSTEMD_UNIT
        unit.unlink(missing_ok=True)
        subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
        console.print("[green]✓[/green] systemd service removed.")

    elif platform == "win32":
        subprocess.run(["schtasks", "/Delete", "/F", "/TN", "LinearCanalMCP"],
                       capture_output=True)
        console.print("[green]✓[/green] Task Scheduler task removed.")
    else:
        console.print("[yellow]Unsupported platform.[/yellow]")
