"""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 platform
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"
def _resolve_ingest_url() -> str:
    """Resolve ingest URL: env > config.json > default (trace.quickcall.dev)."""
    env_url = os.environ.get("QC_TRACE_INGEST_URL")
    if env_url:
        return env_url.rstrip("/ingest")
    config_path = QC_TRACE_DIR / "config.json"
    if config_path.exists():
        try:
            data = json.loads(config_path.read_text())
            url = data.get("ingest_url")
            if url:
                return url.rstrip("/ingest")
        except (json.JSONDecodeError, OSError):
            pass
    return "https://trace.quickcall.dev"

INGEST_URL = _resolve_ingest_url()


# ---------------------------------------------------------------------------
# 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))


def _is_service_active() -> str | None:
    """Check if a launchd/systemd service is managing the daemon.

    Returns a description string if active, None otherwise.
    """
    system = platform.system()
    if system == "Darwin":
        domain = f"gui/{os.getuid()}"
        try:
            result = subprocess.run(
                ["launchctl", "print", f"{domain}/com.quickcall.traced"],
                capture_output=True, text=True,
            )
            if result.returncode == 0:
                return "launchd (com.quickcall.traced)"
        except FileNotFoundError:
            pass
    elif system == "Linux":
        try:
            result = subprocess.run(
                ["systemctl", "--user", "is-active", "--quiet", "quickcall"],
                capture_output=True,
            )
            if result.returncode == 0:
                return "systemd (quickcall.service)"
        except FileNotFoundError:
            pass
    return None


def _kill_all_quickcall_processes() -> int:
    """Kill all running 'quickcall run' processes. Returns count killed."""
    killed = 0
    try:
        result = subprocess.run(
            ["pgrep", "-f", "quickcall run"],
            capture_output=True, text=True,
        )
        if result.returncode == 0:
            for line in result.stdout.strip().splitlines():
                try:
                    pid = int(line.strip())
                    if pid == os.getpid():
                        continue
                    os.kill(pid, signal.SIGTERM)
                    killed += 1
                except (ValueError, ProcessLookupError, PermissionError):
                    pass
    except FileNotFoundError:
        pass
    return killed


def _stop_service() -> bool:
    """Stop the launchd/systemd service. Returns True if a service was stopped."""
    system = platform.system()
    if system == "Darwin":
        domain = f"gui/{os.getuid()}"
        try:
            result = subprocess.run(
                ["launchctl", "bootout", f"{domain}/com.quickcall.traced"],
                capture_output=True, text=True,
            )
            if result.returncode == 0:
                return True
        except FileNotFoundError:
            pass
    elif system == "Linux":
        try:
            result = subprocess.run(
                ["systemctl", "--user", "stop", "quickcall"],
                capture_output=True, text=True,
            )
            if result.returncode == 0:
                return True
        except FileNotFoundError:
            pass
    return False


# ---------------------------------------------------------------------------
# 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}")
        api_key = _load_api_key()
        if api_key:
            req.add_header("X-API-Key", api_key)
        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 _load_api_key() -> str | None:
    """Load API key from config.json."""
    config_path = QC_TRACE_DIR / "config.json"
    if config_path.exists():
        try:
            return json.loads(config_path.read_text()).get("api_key")
        except (json.JSONDecodeError, OSError):
            pass
    return os.environ.get("QC_TRACE_API_KEY")



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

def cmd_run(args: argparse.Namespace) -> int:
    """Run the daemon in the foreground (used by service wrappers)."""
    import logging

    from qc_trace.daemon.config import DaemonConfig
    from qc_trace.daemon.main import Daemon

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    )

    config = DaemonConfig()
    daemon = Daemon(config)
    daemon.run()
    return 0


def _rebootstrap_service() -> bool:
    """Re-register launchd/systemd service if plist/unit exists but service is not active."""
    system = platform.system()
    if system == "Darwin":
        plist = Path.home() / "Library/LaunchAgents/com.quickcall.traced.plist"
        if plist.exists():
            domain = f"gui/{os.getuid()}"
            # Clear uv cache first to prevent corrupted venv crash loops
            subprocess.run(
                ["uv", "cache", "clean", "qc-trace", "--force"],
                capture_output=True,
            )
            subprocess.run(
                ["launchctl", "bootstrap", domain, str(plist)],
                capture_output=True,
            )
            print("Service re-registered with launchd")
            return True
    elif system == "Linux":
        unit = Path.home() / ".config/systemd/user/quickcall.service"
        if unit.exists():
            subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
            subprocess.run(["systemctl", "--user", "start", "quickcall"], capture_output=True)
            print("Service restarted with systemd")
            return True
    return False


def cmd_start(args: argparse.Namespace) -> int:
    """Start the daemon in the background."""
    # Refuse if managed by a system service (launchd/systemd)
    service = _is_service_active()
    if service:
        print(f"Daemon is managed by {service} — use 'quickcall stop' first to disable it")
        return 1

    existing_pid = _read_pid()
    if existing_pid is not None:
        print(f"Daemon already running (PID {existing_pid})")
        return 0

    # If service plist/unit exists but got unloaded (crash loop), re-register it
    if _rebootstrap_service():
        return 0

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

    import shutil

    quickcall_bin = shutil.which("quickcall")
    if quickcall_bin:
        cmd = [quickcall_bin, "run"]
    else:
        cmd = [sys.executable, "-m", "qc_trace.daemon"]

    log_fp = open(LOG_FILE, "a")  # noqa: SIM115
    proc = subprocess.Popen(
        cmd,
        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 (both manual and service-managed)."""
    stopped_something = False

    # 1. Stop system service if active (launchd/systemd)
    service = _is_service_active()
    if service:
        print(f"Stopping {service}...")
        if _stop_service():
            stopped_something = True
            print(f"Service stopped ({service})")
        else:
            print(f"Failed to stop {service}")

    # 2. Kill the PID-file process
    pid = _read_pid()
    if pid is not None:
        try:
            os.kill(pid, signal.SIGTERM)
            # 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:
                try:
                    os.kill(pid, signal.SIGKILL)
                except ProcessLookupError:
                    pass
            stopped_something = True
        except ProcessLookupError:
            pass
        except PermissionError:
            print(f"Permission denied stopping PID {pid}")

    # 3. Kill any remaining quickcall run processes (orphans, wrapper children)
    killed = _kill_all_quickcall_processes()
    if killed:
        stopped_something = True
        print(f"Killed {killed} orphan process(es)")

    PID_FILE.unlink(missing_ok=True)

    if stopped_something:
        print("Daemon stopped")
        return 0
    else:
        print("Daemon is not running")
        return 1


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
        daemon_ver_file = QC_TRACE_DIR / "daemon_version"
        d_ver = None
        if daemon_ver_file.exists():
            try:
                d_ver = daemon_ver_file.read_text().strip()
            except OSError:
                pass
        result = {
            "version": __version__,
            "daemon_version": d_ver,
            "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()
    daemon_version_file = QC_TRACE_DIR / "daemon_version"
    daemon_ver = None
    if daemon_version_file.exists():
        try:
            daemon_ver = daemon_version_file.read_text().strip()
        except OSError:
            pass
    if daemon_ver and daemon_ver != __version__:
        print(f"  QuickCall Trace v{daemon_ver} (daemon) / v{__version__} (cli)")
        print(f"  ℹ Daemon will auto-update to v{__version__} within 5 minutes")
    else:
        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 = INGEST_URL + "/ingest"
    if health:
        print(f"  Server: {ingest_display} \u2713")
    else:
        print(f"  Server: {ingest_display} \u2717")

    # Source table — local-only view from state.json + push_status.json
    if source_stats:
        print()
        print(f"  {'Source':<18}{'Files':>10}{'Lines':>12}{'Last push':>12}")
        print(f"  {'─' * 52}")
        total_files = 0
        total_lines = 0
        for src in sorted(source_stats.keys()):
            stats = source_stats[src]
            display_name = SOURCE_DISPLAY.get(src, src)
            files = stats["sessions"]
            lines = stats["messages"]
            total_files += files
            total_lines += lines
            last_push = stats.get("last_push_at")
            push_str = _format_time_ago(last_push) if last_push else "-"
            print(f"  {display_name:<18}{files:>10,}{lines:>12,}{push_str:>12}")

        print()
        print(f"  {total_files:,} files · {total_lines:,} lines processed")

        # Sync rate from push_status (prefer current session rate)
        last_push_ts = push_status.get("last_push_at")
        session_start = push_status.get("session_start_at")
        session_msgs = push_status.get("messages_this_session", 0)
        if last_push_ts and session_start and session_msgs:
            elapsed_min = (time.time() - session_start) / 60
            if elapsed_min > 0.5:
                rate = session_msgs / elapsed_min
                ago_str = _format_time_ago(last_push_ts)
                print(f"  Rate:  {rate:,.0f} messages/min · synced {ago_str}")

        # Queue/backoff warnings
        queue_size = push_status.get("queue_size", 0)
        current_backoff = push_status.get("current_backoff", 0)
        if queue_size > 0 or current_backoff > 0:
            warn_parts = []
            if queue_size > 0:
                warn_parts.append(f"{queue_size:,} messages queued for retry")
            if current_backoff > 0:
                warn_parts.append(f"backoff {current_backoff}s")
            print(f"  Queue: {' · '.join(warn_parts)}")

        # Errors are sent to server via heartbeat — not shown here
    else:
        print()
        print("  No session data yet.")

    print()
    return 0


def cmd_check_access(args: argparse.Namespace) -> int:
    """Diagnostic: check access to git repos in macOS protected folders."""
    from qc_trace.utils.repo_resolver import resolve_repo

    if platform.system() != "Darwin":
        print("No protected folders to check (not macOS)")
        return 0

    home = Path.home()
    protected_folders = [
        ("Documents", home / "Documents"),
        ("Desktop", home / "Desktop"),
        ("Downloads", home / "Downloads"),
    ]

    print()
    print("  Scanning for git repos in protected folders...")
    print()

    repos_found: list[tuple[Path, str | None, str | None, bool]] = []
    denied_folders: list[str] = []

    for folder_name, folder_path in protected_folders:
        if not folder_path.exists():
            continue
        for depth in range(1, 5):
            pattern = "/".join(["*"] * depth) + "/.git"
            try:
                for git_dir in folder_path.glob(pattern):
                    repo_path = git_dir.parent
                    try:
                        os.listdir(repo_path)
                        repo_info = resolve_repo(str(repo_path))
                        repos_found.append((repo_path, repo_info.git_branch, repo_info.repo_name, True))
                    except PermissionError:
                        repos_found.append((repo_path, None, None, False))
            except PermissionError:
                denied_folders.append(folder_name)
                print(f"  ~/{folder_name}  access denied")
                break

    if not repos_found and not denied_folders:
        print("  No git repos found in protected folders")
        print()
        return 0

    for repo_path, branch, repo_name, accessible in repos_found:
        rel = f"~/{repo_path.relative_to(home)}"
        if accessible and (branch or repo_name):
            info_parts = []
            if branch:
                info_parts.append(f"branch: {branch}")
            if repo_name:
                info_parts.append(f"repo: {repo_name}")
            print(f"  {rel:<40} git OK  ({', '.join(info_parts)})")
        elif accessible:
            print(f"  {rel:<40} git OK  (no remote)")
        else:
            print(f"  {rel:<40} git DENIED")

    if denied_folders:
        print()
        print("  To fix denied folders, reset TCC and re-run the installer:")
        for folder_name in denied_folders:
            tcc_types = {
                "Documents": "SystemPolicyDocumentsFolder",
                "Desktop": "SystemPolicyDesktopFolder",
                "Downloads": "SystemPolicyDownloadsFolder",
            }
            tcc_type = tcc_types.get(folder_name)
            if tcc_type:
                print(f"    tccutil reset {tcc_type} com.quickcall.traced")

    print()
    print("  Note: the daemon auto-backfills git metadata on restart.")
    print("  To trigger now: quickcall stop && quickcall start")
    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



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

def build_parser() -> argparse.ArgumentParser:
    """Build the CLI argument parser."""
    parser = argparse.ArgumentParser(
        prog="quickcall",
        description="QuickCall — AI coding session tracker",
    )
    sub = parser.add_subparsers(dest="command")

    status_parser = sub.add_parser("status", help="Check connection and stats")
    status_parser.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
    sub.add_parser("start", help="Start the daemon")
    sub.add_parser("stop")
    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 (default: 50)")

    sub.add_parser("check-access", help="Check access to git repos in macOS protected folders")

    # Internal (hidden from --help, used by service wrappers)
    sub.add_parser("run")

    return parser


COMMAND_MAP = {
    "run": cmd_run,
    "start": cmd_start,
    "stop": cmd_stop,
    "status": cmd_status,
    "logs": cmd_logs,
    "check-access": cmd_check_access,
}


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

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

    return handler(args)


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