"""quickcall CLI — manage the trace collection daemon.

Commands:
    start   Start the daemon in the background
    stop    Stop a running daemon
    status  Show daemon status and health info
    logs    Tail the daemon log file
    db init Initialize the database schema
"""

from __future__ import annotations

import argparse
import json
import os
import signal
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

QC_TRACE_DIR = Path.home() / ".quickcall-trace"
PID_FILE = QC_TRACE_DIR / "quickcall.pid"
LOG_FILE = QC_TRACE_DIR / "quickcall.log"
INGEST_URL = os.environ.get("QC_TRACE_INGEST_URL", "http://localhost:19777/ingest").rstrip("/ingest")


# ---------------------------------------------------------------------------
# PID helpers
# ---------------------------------------------------------------------------

def _read_pid() -> int | None:
    """Read PID from file. Returns None if missing or stale."""
    if not PID_FILE.exists():
        return None
    try:
        pid = int(PID_FILE.read_text().strip())
    except (ValueError, OSError):
        return None
    # Check if process is actually alive
    try:
        os.kill(pid, 0)
    except ProcessLookupError:
        # Stale PID file
        PID_FILE.unlink(missing_ok=True)
        return None
    except PermissionError:
        # Process exists but we can't signal it — still alive
        return pid
    return pid


def _write_pid(pid: int) -> None:
    """Write PID to file."""
    QC_TRACE_DIR.mkdir(parents=True, exist_ok=True)
    PID_FILE.write_text(str(pid))


# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------

def _http_get(path: str, timeout: float = 3.0) -> dict | None:
    """GET request to the ingest server. Returns parsed JSON or None."""
    try:
        req = urllib.request.Request(f"{INGEST_URL}{path}")
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode())
    except (urllib.error.URLError, OSError, json.JSONDecodeError, ValueError):
        return None


def _http_post(path: str, data: dict | None = None, timeout: float = 5.0) -> dict | None:
    """POST request to the ingest server. Returns parsed JSON or None."""
    body = json.dumps(data or {}).encode()
    try:
        req = urllib.request.Request(
            f"{INGEST_URL}{path}",
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode())
    except (urllib.error.URLError, OSError, json.JSONDecodeError, ValueError):
        return None


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

def cmd_start(args: argparse.Namespace) -> int:
    """Start the daemon in the background."""
    existing_pid = _read_pid()
    if existing_pid is not None:
        print(f"Daemon already running (PID {existing_pid})")
        return 0

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

    log_fp = open(LOG_FILE, "a")  # noqa: SIM115
    proc = subprocess.Popen(
        [sys.executable, "-m", "qc_trace.daemon"],
        stdout=log_fp,
        stderr=log_fp,
        start_new_session=True,
    )
    log_fp.close()
    _write_pid(proc.pid)
    print(f"Daemon started (PID {proc.pid})")
    return 0


def cmd_stop(args: argparse.Namespace) -> int:
    """Stop the running daemon."""
    pid = _read_pid()
    if pid is None:
        print("Daemon is not running")
        return 1

    try:
        os.kill(pid, signal.SIGTERM)
    except ProcessLookupError:
        PID_FILE.unlink(missing_ok=True)
        print("Daemon stopped")
        return 0
    except PermissionError:
        print(f"Permission denied stopping PID {pid}")
        return 1

    # Wait up to 5 seconds for clean exit
    for _ in range(50):
        try:
            os.kill(pid, 0)
        except ProcessLookupError:
            break
        time.sleep(0.1)
    else:
        # Still alive — force kill
        try:
            os.kill(pid, signal.SIGKILL)
        except ProcessLookupError:
            pass

    PID_FILE.unlink(missing_ok=True)
    print("Daemon stopped")
    return 0


SOURCE_DISPLAY = {
    "claude_code": "Claude Code",
    "codex_cli": "Codex CLI",
    "gemini_cli": "Gemini CLI",
    "cursor": "Cursor IDE",
}


def _format_time_ago(ts: float) -> str:
    """Format a timestamp as a human-readable 'X ago' string."""
    elapsed = time.time() - ts
    if elapsed < 60:
        return f"{int(elapsed)}s ago"
    if elapsed < 3600:
        return f"{int(elapsed // 60)}m ago"
    if elapsed < 86400:
        return f"{int(elapsed // 3600)}h ago"
    return f"{int(elapsed // 86400)}d ago"


def _format_uptime(seconds: float) -> str:
    """Format seconds into a human-readable uptime string."""
    days, remainder = divmod(int(seconds), 86400)
    hours, remainder = divmod(remainder, 3600)
    minutes, _ = divmod(remainder, 60)
    parts = []
    if days:
        parts.append(f"{days}d")
    if hours:
        parts.append(f"{hours}h")
    if minutes or not parts:
        parts.append(f"{minutes}m")
    return " ".join(parts)


def _load_state() -> dict:
    """Load state.json and return the files dict."""
    state_file = QC_TRACE_DIR / "state.json"
    if not state_file.exists():
        return {}
    try:
        data = json.loads(state_file.read_text())
        return data.get("files", {})
    except (json.JSONDecodeError, OSError):
        return {}


def _load_push_status() -> dict:
    """Load push_status.json."""
    push_file = QC_TRACE_DIR / "push_status.json"
    if not push_file.exists():
        return {}
    try:
        return json.loads(push_file.read_text())
    except (json.JSONDecodeError, OSError):
        return {}


def cmd_status(args: argparse.Namespace) -> int:
    """Show daemon status with rich display."""
    from qc_trace import __version__

    pid = _read_pid()
    state_files = _load_state()
    push_status = _load_push_status()

    # Build per-source stats from state.json
    source_stats: dict[str, dict] = {}
    for _path, fstate in state_files.items():
        src = fstate.get("source", "unknown")
        if src not in source_stats:
            source_stats[src] = {"sessions": 0, "messages": 0}
        source_stats[src]["sessions"] += 1
        source_stats[src]["messages"] += fstate.get("last_line_processed", 0)

    # Merge push timestamps from push_status.json
    by_source = push_status.get("by_source", {})
    for src, pdata in by_source.items():
        if src not in source_stats:
            source_stats[src] = {"sessions": 0, "messages": 0}
        source_stats[src]["last_push_at"] = pdata.get("last_push_at")
        source_stats[src]["messages_pushed"] = pdata.get("messages_pushed", 0)

    if getattr(args, "json", False):
        config_file = QC_TRACE_DIR / "config.json"
        org_val = None
        if config_file.exists():
            try:
                org_val = json.loads(config_file.read_text()).get("org")
            except (json.JSONDecodeError, OSError):
                pass
        result = {
            "version": __version__,
            "org": org_val,
            "daemon_running": pid is not None,
            "pid": pid,
            "ingest_url": INGEST_URL,
            "sources": source_stats,
            "push_status": push_status,
        }
        if pid:
            try:
                result["uptime_seconds"] = time.time() - PID_FILE.stat().st_mtime
            except OSError:
                pass
        health = _http_get("/health")
        result["server_reachable"] = health is not None
        print(json.dumps(result, indent=2))
        return 0

    # Rich text output
    print()
    print(f"  QuickCall Trace v{__version__}")

    # Org
    config_file = QC_TRACE_DIR / "config.json"
    org = None
    if config_file.exists():
        try:
            org = json.loads(config_file.read_text()).get("org")
        except (json.JSONDecodeError, OSError):
            pass
    if org:
        print(f"  Org: {org}")

    # Daemon status
    if pid:
        uptime_str = ""
        try:
            elapsed = time.time() - PID_FILE.stat().st_mtime
            uptime_str = f" \u00b7 uptime {_format_uptime(elapsed)}"
        except OSError:
            pass
        print(f"  Daemon: running (PID {pid}){uptime_str}")
    else:
        print("  Daemon: not running")

    # Version check from push_status
    daemon_version = push_status.get("daemon_version")
    available_update = push_status.get("available_update")
    if available_update:
        print(f"  Update: {available_update} available (current: {daemon_version or __version__})")

    # Server health
    health = _http_get("/health")
    ingest_display = os.environ.get("QC_TRACE_INGEST_URL", "http://localhost:19777/ingest")
    if health:
        print(f"  Server: {ingest_display} \u2713")
    else:
        print(f"  Server: {ingest_display} \u2717")

    # Source table
    if source_stats:
        print()
        print(f"  {'Source':<18}{'Sessions':>10}{'Messages':>12}{'Last push':>12}")
        print(f"  {'─' * 52}")
        total_sessions = 0
        total_messages = 0
        for src in sorted(source_stats.keys()):
            stats = source_stats[src]
            display_name = SOURCE_DISPLAY.get(src, src)
            sessions = stats["sessions"]
            messages = stats.get("messages_pushed", stats["messages"])
            total_sessions += sessions
            total_messages += messages
            last_push = stats.get("last_push_at")
            push_str = _format_time_ago(last_push) if last_push else "-"
            print(f"  {display_name:<18}{sessions:>10,}{messages:>12,}{push_str:>12}")

        print()
        print(f"  Total: {total_sessions:,} sessions \u00b7 {total_messages:,} messages")
    else:
        print()
        print("  No session data yet.")

    print()
    return 0


def cmd_logs(args: argparse.Namespace) -> int:
    """Tail the daemon log file."""
    if not LOG_FILE.exists():
        print(f"Log file not found: {LOG_FILE}")
        return 1

    num_lines = args.lines

    lines = LOG_FILE.read_text().splitlines()
    for line in lines[-num_lines:]:
        print(line)

    if args.follow:
        try:
            with open(LOG_FILE) as f:
                f.seek(0, 2)  # seek to end
                while True:
                    line = f.readline()
                    if line:
                        print(line, end="")
                    else:
                        time.sleep(0.2)
        except KeyboardInterrupt:
            pass

    return 0


def cmd_setup(args: argparse.Namespace) -> int:
    """Prompt for email and API key, store in ~/.quickcall-trace/config.json."""
    config_path = QC_TRACE_DIR / "config.json"
    existing: dict = {}
    if config_path.exists():
        try:
            existing = json.loads(config_path.read_text())
        except (json.JSONDecodeError, OSError):
            pass

    email = input(f"Email [{existing.get('email', '')}]: ").strip()
    if not email:
        email = existing.get("email", "")
    if not email:
        print("Email is required.")
        return 1

    api_key = input(f"API key [{existing.get('api_key', '')[:8] + '...' if existing.get('api_key') else ''}]: ").strip()
    if not api_key:
        api_key = existing.get("api_key", "")
    if not api_key:
        print("API key is required.")
        return 1

    QC_TRACE_DIR.mkdir(parents=True, exist_ok=True)
    config = {**existing, "email": email, "api_key": api_key}
    config_path.write_text(json.dumps(config, indent=2) + "\n")
    print(f"Config saved to {config_path}")
    return 0


def cmd_db_init(args: argparse.Namespace) -> int:
    """Initialize the database schema."""
    result = _http_post("/db/init")
    if result is not None:
        print("Database schema initialized")
        return 0

    print("Failed to initialize database (server unreachable)")
    return 1


# ---------------------------------------------------------------------------
# Argument parser
# ---------------------------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    """Build the CLI argument parser."""
    parser = argparse.ArgumentParser(
        prog="quickcall",
        description="Manage the QuickCall trace collection daemon",
    )
    sub = parser.add_subparsers(dest="command")

    # setup
    sub.add_parser("setup", help="Configure email and API key")

    # start
    sub.add_parser("start", help="Start the daemon")

    # stop
    sub.add_parser("stop", help="Stop the daemon")

    # status
    status_parser = sub.add_parser("status", help="Show daemon status")
    status_parser.add_argument(
        "--json", action="store_true", help="Output as JSON"
    )

    # logs
    logs_parser = sub.add_parser("logs", help="Tail daemon logs")
    logs_parser.add_argument(
        "-f", "--follow", action="store_true", help="Follow log output"
    )
    logs_parser.add_argument(
        "-n", "--lines", type=int, default=50,
        help="Number of lines to show (default: 50)",
    )

    # db
    db_parser = sub.add_parser("db", help="Database commands")
    db_sub = db_parser.add_subparsers(dest="db_command")
    db_sub.add_parser("init", help="Initialize database schema")

    return parser


COMMAND_MAP = {
    "setup": cmd_setup,
    "start": cmd_start,
    "stop": cmd_stop,
    "status": cmd_status,
    "logs": cmd_logs,
}


def main(argv: list[str] | None = None) -> int:
    """CLI entry point."""
    parser = build_parser()
    args = parser.parse_args(argv)

    if args.command is None:
        parser.print_help()
        return 1

    if args.command == "db":
        if getattr(args, "db_command", None) == "init":
            return cmd_db_init(args)
        parser.parse_args(["db", "--help"])
        return 1

    handler = COMMAND_MAP.get(args.command)
    if handler is None:
        parser.print_help()
        return 1

    return handler(args)


if __name__ == "__main__":
    sys.exit(main())
