"""Command-line interface and command handlers for the WolfPanel Agent.

Commands:
  run                     run as a service (heartbeat loop + initial discovery)
  register --token X      register with the Central API using an install token
  heartbeat               send a single heartbeat
  discover --quick|--full run discovery (read-only)
  update-check            report whether a newer version is available
  status                  print local agent status
  version                 print the agent version
"""

from __future__ import annotations

import argparse
import json
import logging
import time

from api_client import ApiError, get_api_client
from config import AGENT_VERSION, load_config
from discovery import full as full_discovery
from discovery import quick as quick_discovery
from heartbeat import send_once
from identity import (
    ensure_dirs,
    load_agent_token,
    store_credentials,
)
from logger import get_logger, setup_logging
from update import check as update_check, self_update

log = get_logger("cli")


# --- Command handlers -------------------------------------------------------

def cmd_register(config, args) -> int:
    """Exchange an install token for long-lived agent credentials.

    SECURITY: the install token is used in-memory only and is never written to
    disk. Only the returned agent/refresh tokens are persisted.
    """
    install_token = args.token
    if not install_token:
        # Pairing-flow placeholder. Browser pairing is not implemented in v1;
        # we only show the structure the production flow will fill in. The
        # connect URL is configurable (WOLFPANEL_CONNECT_URL) so we never print
        # a dead, hardcoded domain.
        print("\nNo install token provided.\n")
        if config.connect_url:
            print(f"Open:\n  {config.connect_url}\n")
        else:
            print("Open: <pairing URL not configured; set WOLFPANEL_CONNECT_URL>\n")
        print("Pairing Code:\n  DEV-XXXX\n")
        print("(Browser pairing is not implemented in v1 — pass --token to register.)")
        return 1

    ensure_dirs(config)
    api_client = get_api_client(config)

    os_info = quick_discovery.system.os_info()
    # Register request body = frozen backend schema (AgentRegisterRequest).
    payload = {
        "install_token": install_token,
        "agent_version": config.agent_version,
        "hostname": quick_discovery.system.hostname(),
        "os_name": os_info.get("distro_id") or os_info.get("system"),
        "os_version": os_info.get("distro_version") or os_info.get("release"),
        "cpu_cores": quick_discovery.system.cpu_count(),
        "memory_total": quick_discovery.system.memory_total_bytes(),
    }

    try:
        response = api_client.register(payload)
    except ApiError as exc:
        log.error("registration failed: %s", exc)
        return 1

    # Register response = {agent_token, server_id, status}; no agent_id /
    # refresh_token in v1.
    agent_token = response.get("agent_token")
    server_id = response.get("server_id")
    if not (agent_token and server_id is not None):
        log.error("registration response missing required fields")
        return 1

    store_credentials(config, str(server_id), agent_token)
    # install_token goes out of scope here; never persisted. TODO(prod): also
    # scrub it from process memory more aggressively.
    print(f"Registered successfully. server_id={server_id} status={response.get('status')}")
    return 0


def cmd_heartbeat(config, args) -> int:
    api_client = get_api_client(config)
    return 0 if send_once(config, api_client) else 1


def cmd_discover(config, args) -> int:
    if args.metrics:
        import metrics
        snapshot = metrics.get_metrics()
        print(json.dumps(snapshot, indent=2, default=str))
        return 0
    if args.quick and not args.full:
        snapshot = quick_discovery.collect()
        print(json.dumps(snapshot, indent=2, default=str))
        return 0
    # Default to full discovery (also when --full given).
    api_client = get_api_client(config)
    snapshot = full_discovery.run(config, api_client, upload=not args.no_upload)
    print(json.dumps(snapshot["discovery"], indent=2, default=str))
    return 0


def cmd_update_check(config, args) -> int:
    api_client = get_api_client(config)
    info = update_check(config, api_client)
    if info is None:
        print("Update check failed (see logs).")
        return 1
    if info.update_available:
        print(f"Update available: {info.current} -> {info.latest}")
    else:
        print(f"Up to date: {info.current}")
    return 0


def cmd_update(config, args) -> int:
    """Check for update and perform self-update."""
    if args.version:
        print(config.agent_version)
        return 0

    api_client = get_api_client(config)
    info = update_check(config, api_client)
    if info is None:
        if not args.check:
            print("Update check failed.")
        return 1

    if args.check:
        print(f"current: {info.current}")
        print(f"latest: {info.latest}")
        return 2 if info.update_available else 0

    print(f"Current version: v{info.current}")

    if not info.update_available:
        print(f"Agent is up to date (v{info.current})")
        return 0

    print(f"Update available: v{info.current} -> v{info.latest}")

    if not args.yes:
        try:
            choice = input("Proceed with update? [y/N]: ").strip().lower()
            if choice not in ("y", "yes"):
                print("Update cancelled.")
                return 0
        except (KeyboardInterrupt, EOFError):
            print("\nUpdate cancelled.")
            return 0

    print("Initiating agent update...")
    try:
        self_update(config, info)
        print(f"Agent updated to v{info.latest}")
        return 0
    except Exception as exc:
        print(f"Update failed: {exc}")
        return 1


def cmd_status(config, args) -> int:
    token = load_agent_token(config)
    state = "registered" if (config.server_id and token) else "pending_install"
    print(f"server_id:  {config.server_id or '<unregistered>'}")
    print(f"version:    {config.agent_version}")
    print(f"channel:    {config.release_channel}")
    print(f"api_url:    {config.api_url}")
    print(f"api_mock:   {config.api_mock}")
    print(f"token:      {'present' if token else 'missing'}")
    print(f"state:      {state}")
    return 0


def cmd_version(config, args) -> int:
    print(AGENT_VERSION)
    return 0


def cmd_run(config, args) -> int:
    """Service mode: one initial discovery, then a resilient heartbeat loop.

    The service must never exit just because registration has not happened yet.
    If credentials are missing/pending it keeps running and retries on each
    interval instead of crash-looping (systemd Restart=always).
    """
    ensure_dirs(config)
    api_client = get_api_client(config)
    log.info("agent starting (version=%s, interval=%ss)", config.agent_version,
             config.heartbeat_interval)

    token = load_agent_token(config)
    if config.registration_pending or not (config.server_id and token):
        log.warning(
            "agent not registered yet (pending=%s); running in degraded mode and "
            "retrying. Re-run 'register --token <TOKEN>' to complete registration.",
            config.registration_pending,
        )

    # Initial discovery on startup (best-effort). Quick scan first so the panel
    # gets instant server_facts, then the slower full inventory.
    try:
        quick_discovery.run(config, api_client, upload=True)
    except Exception as exc:  # noqa: BLE001 - never let discovery kill the loop
        log.warning("initial quick scan failed: %s", exc)
    try:
        full_discovery.run(config, api_client, upload=True)
    except Exception as exc:  # noqa: BLE001 - never let discovery kill the loop
        log.warning("initial discovery failed: %s", exc)

    try:
        from command_runner import process_commands
        import metrics
        metrics.start_updater(interval=config.heartbeat_interval)
        while True:
            send_once(config, api_client)
            try:
                process_commands(config, api_client)
            except Exception as exc:  # noqa: BLE001
                log.warning("command processor failed: %s", exc)
            time.sleep(config.heartbeat_interval)
    except KeyboardInterrupt:
        log.info("agent stopping (interrupt)")
        import metrics
        metrics.stop_updater()
        return 0


# --- Argument parsing -------------------------------------------------------

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="wolfpanel-agent", description="WolfPanel Agent")
    parser.add_argument("--verbose", action="store_true", help="debug-level logging")
    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("run", help="run as a service (heartbeat loop)")

    p_register = sub.add_parser("register", help="register with the Central API")
    p_register.add_argument("--token", help="install token (omit to see pairing placeholder)")

    sub.add_parser("heartbeat", help="send a single heartbeat")

    p_discover = sub.add_parser("discover", help="run read-only discovery")
    p_discover.add_argument("--quick", action="store_true", help="quick baseline snapshot")
    p_discover.add_argument("--full", action="store_true", help="full inventory (default)")
    p_discover.add_argument("--metrics", action="store_true", help="collect telemetry metrics")
    p_discover.add_argument("--no-upload", action="store_true", help="do not upload snapshot")

    sub.add_parser("update-check", help="check the release channel for updates")

    p_update = sub.add_parser("update", help="self-update the agent")
    p_update.add_argument("--yes", action="store_true", help="skip confirmation")
    p_update.add_argument("--check", action="store_true", help="check only")
    p_update.add_argument("--version", action="store_true", help="print current version")

    sub.add_parser("status", help="print local agent status")
    sub.add_parser("version", help="print the agent version")
    return parser


_HANDLERS = {
    "run": cmd_run,
    "register": cmd_register,
    "heartbeat": cmd_heartbeat,
    "discover": cmd_discover,
    "update-check": cmd_update_check,
    "update": cmd_update,
    "status": cmd_status,
    "version": cmd_version,
}


def main(argv: list[str] | None = None) -> int:
    import os
    try:
        os.umask(0o077)
    except Exception:
        pass
    args = build_parser().parse_args(argv)
    config = load_config()
    setup_logging(config.agent_log, level=logging.DEBUG if args.verbose else logging.INFO)
    return _HANDLERS[args.command](config, args)
