from __future__ import annotations

import asyncio
import os
import subprocess
import sys
from multiprocessing import Process
from pathlib import Path
import signal
import json
import socket
from typing import Optional, Tuple

import click
import pyperclip

from . import __version__
from .data import get_pid_file, is_process_running
from .keypair import (
    get_or_create_keypair,
    fingerprint_public_key,
    generate_in_memory_keypair,
    keypair_files_exist,
)
from .pairing import PairingError, pair_device_with_code
from .connection.client import ConnectionManager, run_until_interrupt
from .updater import build_pip_install_command, run_pip_install_command
from .utils.runtime_paths import expand_runtime_path

GATEWAY_URL = "wss://portacode.com/gateway"
GATEWAY_ENV = "PORTACODE_GATEWAY"
MAX_PROJECT_PATHS = 10


@click.group()
@click.version_option(__version__, "-v", "--version", message="Portacode %(version)s")
def cli() -> None:
    """Portacode command-line interface."""


@cli.command()
@click.option("--gateway", "gateway", "-g", help="Gateway websocket URL (overrides env/ default)")
@click.option("--detach", "detach", "-d", is_flag=True, help="Run connection in background")
@click.option("--debug", "debug", is_flag=True, help="Enable debug logging")
@click.option("--log-categories", "log_categories", help="Comma-separated list of log categories to show (e.g., 'connection,auth,git'). Use 'list' to see available categories.")
@click.option("--non-interactive", "non_interactive", is_flag=True, envvar="PORTACODE_NON_INTERACTIVE", hidden=True,
              help="Skip interactive prompts (used by background service)")
@click.option("--pairing-code", "pairing_code_opt", envvar="PORTACODE_PAIRING_CODE", help="Provide a temporary pairing code for zero-copy onboarding")
@click.option("--device-name", "device_name_opt", envvar="PORTACODE_DEVICE_NAME", help="Custom device name to display during pairing")
@click.option(
    "--project-path",
    "project_paths_opt",
    multiple=True,
    help="Project folder to register during pairing (repeat for multiple paths)",
)
@click.option(
    "--setup-proxmox-infra",
    "setup_proxmox_infra",
    is_flag=True,
    help="After authentication, configure this device as a Portacode Proxmox infra node",
)
@click.option(
    "--proxmox-template-family",
    "proxmox_template_families",
    multiple=True,
    help="Template family to prefetch during Proxmox setup (repeat for multiple families)",
)
@click.option(
    "--proxmox-verify-ssl",
    "proxmox_verify_ssl",
    is_flag=True,
    help="Verify SSL certificates during Proxmox API validation",
)
@click.option(
    "--proxmox-setup-retries",
    "proxmox_setup_retries",
    default=3,
    show_default=True,
    type=click.IntRange(1, 10),
    help="Retry Proxmox setup this many times when failures look transient",
)
@click.option(
    "--proxmox-setup-retry-delay",
    "proxmox_setup_retry_delay",
    default=5.0,
    show_default=True,
    type=click.FloatRange(0.0, 60.0),
    help="Seconds to wait between Proxmox setup retries",
)
def connect(
    gateway: Optional[str],
    detach: bool,
    debug: bool,
    log_categories: Optional[str],
    non_interactive: bool,
    pairing_code_opt: Optional[str],
    device_name_opt: Optional[str],
    project_paths_opt: Tuple[str, ...],
    setup_proxmox_infra: bool,
    proxmox_template_families: Tuple[str, ...],
    proxmox_verify_ssl: bool,
    proxmox_setup_retries: int,
    proxmox_setup_retry_delay: float,
) -> None:  # noqa: D401 – Click callback
    """Connect this machine to Portacode gateway."""

    # Set up debug logging if requested
    if debug:
        import logging
        from .logging_categories import configure_logging_categories, parse_category_string, list_available_categories
        
        # Handle log categories
        if log_categories == "list":
            click.echo(click.style("Available log categories:", fg="cyan"))
            for cat in list_available_categories():
                click.echo(f"  • {cat}")
            return
        
        enabled_categories = set()
        if log_categories:
            try:
                enabled_categories = parse_category_string(log_categories)
                configure_logging_categories(enabled_categories)
                click.echo(click.style(f"🔍 Debug logging enabled for categories: {', '.join(sorted(enabled_categories))}", fg="yellow"))
            except ValueError as e:
                click.echo(click.style(f"Error: {e}", fg="red"))
                return
        else:
            click.echo(click.style("🔍 Debug logging enabled (all categories)", fg="yellow"))
        
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )

    # 1. Ensure only a single connection per user
    pid_file = get_pid_file()
    if pid_file.exists():
        try:
            other_pid = int(pid_file.read_text())
        except ValueError:
            other_pid = None

        if other_pid and is_process_running(other_pid):
            if other_pid == os.getpid():
                # We just wrote our own PID (stale pidfile from previous run) – ignore safely.
                click.echo(
                    click.style(
                        "Detected stale portacode pid file referencing the current process; ignoring.",
                        fg="bright_black",
                    )
                )
                pid_file.unlink(missing_ok=True)
            else:
                click.echo(
                    click.style(
                        f"Another portacode connection (PID {other_pid}) is active.", fg="yellow"
                    )
                )
                if non_interactive:
                    click.echo(click.style("Non-interactive mode: terminating the existing connection automatically.", fg="bright_black"))
                    _terminate_process(other_pid)
                    pid_file.unlink(missing_ok=True)
                elif click.confirm("Terminate the existing connection?", default=False):
                    _terminate_process(other_pid)
                    pid_file.unlink(missing_ok=True)
                else:
                    click.echo("Aborting.")
                    sys.exit(1)
        else:
            # Stale pidfile
            pid_file.unlink(missing_ok=True)

    # Determine gateway URL
    target_gateway = gateway or os.getenv(GATEWAY_ENV) or GATEWAY_URL

    pairing_code = pairing_code_opt.strip() if pairing_code_opt else None
    device_name = device_name_opt.strip() if device_name_opt else None
    normalized_project_paths: list[str] = []
    for raw in project_paths_opt:
        if not raw:
            continue
        cleaned = raw.strip()
        if not cleaned:
            continue
        if len(cleaned) > 500:
            click.echo(click.style(f"Project path too long (max 500 chars): {cleaned[:60]}…", fg="red"))
            sys.exit(1)
        cleaned = expand_runtime_path(cleaned)
        if len(cleaned) > 500:
            click.echo(click.style(f"Expanded project path too long (max 500 chars): {cleaned[:60]}…", fg="red"))
            sys.exit(1)
        if cleaned not in normalized_project_paths:
            normalized_project_paths.append(cleaned)
    if len(normalized_project_paths) > MAX_PROJECT_PATHS:
        click.echo(
            click.style(
                f"Too many project paths provided ({len(normalized_project_paths)}). "
                f"Maximum allowed is {MAX_PROJECT_PATHS}.",
                fg="red",
            )
        )
        sys.exit(1)

    normalized_template_families: list[str] = []
    for raw in proxmox_template_families:
        cleaned = raw.strip().lower()
        if cleaned and cleaned not in normalized_template_families:
            normalized_template_families.append(cleaned)

    pairing_requested = bool(pairing_code)
    existing_identity = keypair_files_exist()
    should_pair = pairing_requested

    # 2. Load or create keypair (in memory if pairing)
    if should_pair and not existing_identity:
        keypair = generate_in_memory_keypair()
    else:
        keypair = get_or_create_keypair()

    if should_pair:
        if not device_name:
            device_name = socket.gethostname() or "Portacode Device"
        click.echo(click.style(f"🔐 Pairing code detected; pairing as '{device_name}'...", fg="cyan"))
        try:
            pair_device_with_code(
                keypair,
                pairing_code=pairing_code,
                device_name=device_name,
                project_paths=normalized_project_paths or None,
            )
        except PairingError as exc:
            click.echo(click.style(f"Pairing failed: {exc}", fg="red"))
            sys.exit(1)
        # Persist only when we generated a fresh in-memory identity for pairing.
        if hasattr(keypair, "persist"):
            keypair = keypair.persist()
        click.echo(click.style("✅ Pairing approved. Continuing with connection…", fg="green"))

    fingerprint = fingerprint_public_key(keypair.public_key_pem)

    pubkey_b64 = keypair.public_key_der_b64()
    if not non_interactive and not pairing_requested:
        # Show key generation status
        if getattr(keypair, '_is_new', False):
            click.echo()
            click.echo(click.style("✔ Generated new RSA keypair", fg="green"))
        else:
            click.echo()
            click.echo(click.style("✔ Loaded existing RSA keypair", fg="green"))
        
        # Show key location
        key_dir = getattr(keypair, '_key_dir', None)
        if key_dir:
            click.echo(click.style(f"  📁 Key files: {key_dir}", fg="bright_black"))

        click.echo()
        click.echo(click.style("🚀 Welcome to Portacode!", fg="bright_blue", bold=True))
        click.echo()
        click.echo(click.style("📱 Next steps:", fg="bright_cyan", bold=True))
        click.echo(click.style("  1. Visit ", fg="white") + click.style("https://portacode.com", fg="bright_blue", underline=True))
        click.echo(click.style("  2. Create your free account or sign in", fg="white"))
        click.echo(click.style("  3. Add this device using the key below", fg="white"))
        click.echo()
        
        click.echo(click.style("🔑 Device Key:", fg="bright_yellow", bold=True))
        
        # Dynamic border based on key length for visual appeal
        max_width = 80  # Maximum width for terminal display
        if len(pubkey_b64) <= max_width - 4:
            # Single line display
            border_width = len(pubkey_b64) + 4
            top = "┌" + "─" * (border_width - 2) + "┐"
            middle = "│ " + pubkey_b64 + " │"
            bottom = "└" + "─" * (border_width - 2) + "┘"
        else:
            # Multi-line display
            line_width = max_width - 4  # Account for border
            lines = []
            for i in range(0, len(pubkey_b64), line_width):
                lines.append(pubkey_b64[i:i+line_width])
            
            top = "┌" + "─" * (max_width - 2) + "┐"
            bottom = "└" + "─" * (max_width - 2) + "┘"
            
            middle_lines = []
            for line in lines:
                padded_line = line.ljust(line_width)
                middle_lines.append("│ " + padded_line + " │")
            middle = "\n".join(middle_lines)
        
        click.echo(click.style(top, fg="bright_black"))
        click.echo(click.style(middle, fg="bright_white"))
        click.echo(click.style(bottom, fg="bright_black"))
        
        # Show clean key for manual copying
        click.echo()
        click.echo(click.style("📋 Copy this key (select all text between the lines):", fg="bright_cyan", bold=True))
        click.echo(click.style("─" * min(len(pubkey_b64), 80), fg="bright_black"))
        click.echo(click.style(pubkey_b64, fg="bright_white", bold=True))
        click.echo(click.style("─" * min(len(pubkey_b64), 80), fg="bright_black"))
        
        # Better clipboard handling with multiple fallback mechanisms
        clipboard_success = False
        clipboard_error = None
        
        try:
            pyperclip.copy(pubkey_b64)
            # Verify the copy actually worked
            if pyperclip.paste() == pubkey_b64:
                clipboard_success = True
            else:
                clipboard_error = "Copy verification failed"
        except Exception as e:
            clipboard_error = str(e)
        
        if clipboard_success:
            click.echo(click.style("✅ Copied to clipboard!", fg="bright_green"))
        else:
            click.echo(click.style("⚠️  Copy manually from above", fg="bright_yellow"))
            if clipboard_error and "could not find a copy/paste mechanism" in clipboard_error:
                click.echo(click.style("   💡 Try: sudo apt-get install xclip", fg="bright_black"))
        
        click.echo(click.style(f"   Fingerprint: {fingerprint}", fg="bright_black"))
        click.echo()
        
        click.echo(click.style("💡 Connection info:", fg="bright_magenta", bold=True))
        click.echo(click.style("   • This session: Connection active until terminal closes", fg="white"))
        click.echo(click.style("   • Permanent: Run ", fg="white") + click.style("portacode service install", fg="bright_cyan") + click.style(" for auto-connect", fg="white"))
        click.echo(click.style("   • More help: ", fg="white") + click.style("portacode service --help", fg="bright_cyan"))
        click.echo()
        
        click.prompt(click.style("Press Enter once device is added", fg="bright_green", bold=True), default="", show_default=False)

    if setup_proxmox_infra:
        payload: dict[str, object] = {
            "cmd": "setup_proxmox_infra",
            "token_mode": "auto_admin",
            "verify_ssl": proxmox_verify_ssl,
        }
        if normalized_template_families:
            payload["template_families"] = normalized_template_families

        last_reply = None
        success = False
        for attempt in range(1, proxmox_setup_retries + 1):
            click.echo()
            click.echo(
                click.style(
                    f"⚙ Running Proxmox infrastructure setup (attempt {attempt}/{proxmox_setup_retries})…",
                    fg="bright_cyan",
                )
            )
            success, last_reply = _run_control_command(
                payload,
                gateway=target_gateway,
                keypair=keypair,
                debug=debug,
                label="Waiting for Proxmox setup response",
                wait_loops=900,
                success_events={"proxmox_infra_configured"},
                failure_events={"error"},
            )
            if success:
                click.echo(click.style("✅ Proxmox infrastructure setup completed.", fg="green"))
                break
            if attempt >= proxmox_setup_retries or not _is_transient_proxmox_setup_failure(last_reply):
                click.echo(click.style("Proxmox infrastructure setup failed.", fg="red"))
                sys.exit(1)
            click.echo(
                click.style(
                    f"Transient setup failure detected; retrying in {proxmox_setup_retry_delay:g}s…",
                    fg="yellow",
                )
            )
            asyncio.run(asyncio.sleep(proxmox_setup_retry_delay))

    # 3. Start connection manager
    if detach and not non_interactive:
        click.echo("Establishing connection in the background…")
        p = Process(
            target=_run_connection_forever,
            args=(target_gateway, keypair, pid_file, normalized_project_paths, debug),
        )
        p.daemon = False  # We want it to live beyond parent process on POSIX; on Windows it's anyway independent
        p.start()
        click.echo(click.style(f"Background process PID: {p.pid}", fg="green"))
        return

    # Foreground mode → run in current event-loop
    if not detach:
        pid_file.write_text(str(os.getpid()))

    async def _main() -> None:
        mgr = ConnectionManager(
            target_gateway,
            keypair,
            debug=debug,
            initial_project_paths=normalized_project_paths,
        )
        await run_until_interrupt(mgr)

    try:
        asyncio.run(_main())
    finally:
        pid_file.unlink(missing_ok=True)


def _run_connection_forever(
    url: str,
    keypair,
    pid_file: Path,
    initial_project_paths: Optional[list[str]] = None,
    debug: bool = False,
):
    """Entry-point for detached background process."""
    try:
        pid_file.write_text(str(os.getpid()))

        async def _main() -> None:
            mgr = ConnectionManager(
                url,
                keypair,
                debug=debug,
                initial_project_paths=initial_project_paths,
            )
            await run_until_interrupt(mgr)

        asyncio.run(_main())
    finally:
        pid_file.unlink(missing_ok=True)


def _terminate_process(pid: int):
    if sys.platform.startswith("win"):
        import ctypes
        PROCESS_TERMINATE = 1
        handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, pid)
        if handle:
            ctypes.windll.kernel32.TerminateProcess(handle, -1)
            ctypes.windll.kernel32.CloseHandle(handle)
    else:
        try:
            os.kill(pid, signal.SIGTERM)  # type: ignore[name-defined]
        except OSError:
            pass


def _is_transient_proxmox_setup_failure(reply: object) -> bool:
    if not isinstance(reply, dict):
        return True
    message = str(reply.get("message") or reply.get("error") or "").lower()
    if not message:
        return True
    transient_markers = (
        "temporary",
        "temporarily",
        "timed out",
        "timeout",
        "network",
        "connection",
        "name resolution",
        "could not resolve",
        "unreachable",
        "tls",
        "ssl",
        "503",
        "502",
        "504",
        "download",
        "fetch",
    )
    return any(marker in message for marker in transient_markers)


def _run_control_command(
    payload: dict,
    gateway: str,
    *,
    keypair=None,
    debug: bool = False,
    label: str = "Waiting for replies",
    wait_loops: int = 50,
    success_events: Optional[set[str]] = None,
    failure_events: Optional[set[str]] = None,
) -> tuple[bool, Optional[dict]]:
    async def _run() -> tuple[bool, Optional[dict]]:
        selected_keypair = keypair or get_or_create_keypair()
        mgr = ConnectionManager(gateway, selected_keypair, debug=debug)
        await mgr.start()

        authenticated = await mgr.wait_until_authenticated(timeout=2.0)
        if not authenticated or mgr.mux is None:
            click.echo("Failed to initialise connection – aborting.")
            await mgr.stop()
            return False, None

        ctl = mgr.mux.get_channel(0)
        await ctl.send(payload)

        last_reply: Optional[dict] = None
        succeeded = success_events is None
        try:
            with click.progressbar(length=wait_loops, label=label) as bar:
                for _ in range(wait_loops):
                    try:
                        reply = await asyncio.wait_for(ctl.recv(), timeout=0.1)
                        click.echo(click.style("< " + json.dumps(reply, indent=2), fg="cyan"))
                        if isinstance(reply, dict):
                            last_reply = reply
                            event_name = str(reply.get("event") or "")
                            if failure_events and event_name in failure_events:
                                return False, last_reply
                            if success_events and event_name in success_events:
                                succeeded = True
                                return True, last_reply
                    except asyncio.TimeoutError:
                        pass
                    bar.update(1)
        finally:
            await mgr.stop()

        return succeeded, last_reply

    return asyncio.run(_run())


# ---------------------------------------------------------------------------
# Debug helpers – NOT intended for production use
# ---------------------------------------------------------------------------


@cli.command()
@click.argument("message", nargs=1)
@click.option("--gateway", "gateway", "-g", help="Gateway websocket URL (overrides env/ default)")
def send_control(message: str, gateway: Optional[str]) -> None:  # noqa: D401 – Click callback
    """Send a raw JSON *control* message on channel 0 and print replies.

    Example::

        portacode send-control '{"cmd": "system_info"}'

    The command opens a short-lived connection, authenticates, sends the
    control message and waits up to 5 seconds for responses which are then
    pretty-printed to stdout.
    """

    try:
        payload = json.loads(message)
    except json.JSONDecodeError as exc:
        raise click.BadParameter(f"Invalid JSON: {exc}") from exc

    target_gateway = gateway or os.getenv(GATEWAY_ENV) or GATEWAY_URL

    async def _run() -> None:
        keypair = get_or_create_keypair()
        mgr = ConnectionManager(target_gateway, keypair, debug=debug)
        await mgr.start()

        authenticated = await mgr.wait_until_authenticated(timeout=2.0)
        if not authenticated or mgr.mux is None:
            click.echo("Failed to initialise connection – aborting.")
            await mgr.stop()
            return

        # Send control frame on channel 0
        ctl = mgr.mux.get_channel(0)
        await ctl.send(payload)

        # Print replies for a short time
        try:
            with click.progressbar(length=50, label="Waiting for replies") as bar:
                for _ in range(50):
                    try:
                        reply = await asyncio.wait_for(ctl.recv(), timeout=0.1)
                        click.echo(click.style("< " + json.dumps(reply, indent=2), fg="cyan"))
                    except asyncio.TimeoutError:
                        pass
                    bar.update(1)
        finally:
            await mgr.stop()

        asyncio.run(_run())


@cli.command("revert_proxmox_infra")
@click.option("--gateway", "gateway", "-g", help="Gateway websocket URL (overrides env/ default)")
def revert_proxmox_infra(gateway: Optional[str]) -> None:  # noqa: D401 – Click callback
    """Revert the Proxmox infrastructure configuration stored on this device."""

    target_gateway = gateway or os.getenv(GATEWAY_ENV) or GATEWAY_URL

    async def _run() -> None:
        keypair = get_or_create_keypair()
        mgr = ConnectionManager(target_gateway, keypair, debug=False)
        await mgr.start()

        authenticated = await mgr.wait_until_authenticated(timeout=2.0)
        if not authenticated or mgr.mux is None:
            click.echo("Failed to initialise connection – aborting.")
            await mgr.stop()
            return

        ctl = mgr.mux.get_channel(0)
        await ctl.send({"cmd": "revert_proxmox_infra"})

        try:
            with click.progressbar(length=30, label="Waiting for revert response") as bar:
                for _ in range(30):
                    try:
                        reply = await asyncio.wait_for(ctl.recv(), timeout=0.1)
                        click.echo(click.style("< " + json.dumps(reply, indent=2), fg="cyan"))
                    except asyncio.TimeoutError:
                        pass
                    bar.update(1)
        finally:
            await mgr.stop()

    asyncio.run(_run())


# ---------------------------------------------------------------------------
# Service sub-commands (install/uninstall/start/stop/status)
# ---------------------------------------------------------------------------


@cli.group()
def service() -> None:  # noqa: D401 – Click callback
    """Manage background *service* that auto-runs ``portacode connect`` on login."""


@service.command("install")
def service_install() -> None:  # noqa: D401
    """Install + enable the background service and start it now (Linux: system service only)."""
    from .service import get_manager

    mgr = get_manager(system_mode=True)
    click.echo(f"Installing Portacode system service…")
    if os.geteuid() != 0:
        click.echo(click.style("[sudo] You may be prompted for your password to install the system service.", fg="yellow"))
        click.echo(click.style("💡 For persistent connection, install system-wide: sudo pip install portacode --system", fg="bright_black"))
    try:
        mgr.install()
        st = mgr.status()
        if st in {"active", "running"}:
            click.echo(click.style("✔ Service installed and running", fg="green"))
        else:
            click.echo(click.style(f"⚠ Service installed but status: {st}", fg="yellow"))
            if hasattr(mgr, "log_path"):
                click.echo(f"Inspect log: {mgr.log_path}")
    except Exception as exc:
        click.echo(click.style(f"Failed: {exc}", fg="red"))
        if "No module named" in str(exc) or "command not found" in str(exc):
            click.echo(click.style("💡 Try installing system-wide: sudo pip install portacode --system", fg="bright_cyan"))


@service.command("uninstall")
def service_uninstall() -> None:  # noqa: D401
    """Stop + remove the background service (Linux: system service only)."""
    from .service import get_manager

    mgr = get_manager(system_mode=True)
    click.echo(f"Uninstalling Portacode system service…")
    try:
        mgr.uninstall()
        click.echo(click.style("✔ Service removed", fg="green"))
    except Exception as exc:
        click.echo(click.style(f"Failed: {exc}", fg="red"))


@service.command("start")
def service_start() -> None:  # noqa: D401
    """Start the service if installed (Linux: system service only)."""
    from .service import get_manager

    mgr = get_manager(system_mode=True)
    try:
        mgr.start()
        st = mgr.status()
        if st in {"active", "running"}:
            click.echo(click.style("Service started and running", fg="green"))
        else:
            click.echo(click.style(f"Service start issued but current status: {st}", fg="yellow"))
            if hasattr(mgr, "log_path"):
                click.echo(f"Inspect log: {mgr.log_path}")
    except Exception as exc:
        click.echo(click.style(f"Failed: {exc}", fg="red"))


@service.command("restart")
def service_restart() -> None:  # noqa: D401
    """Restart the service (Linux: system service only)."""
    try:
        _restart_service_and_report()
    except Exception as exc:
        click.echo(click.style(f"Failed: {exc}", fg="red"))


@service.command("stop")
def service_stop() -> None:  # noqa: D401
    """Stop the service if running (Linux: system service only)."""
    from .service import get_manager

    mgr = get_manager(system_mode=True)
    try:
        mgr.stop()
        click.echo(click.style("Service stopped", fg="green"))
    except Exception as exc:
        click.echo(click.style(f"Failed: {exc}", fg="red"))


@service.command("status")
@click.option("--verbose", "verbose", "-v", is_flag=True, help="Show detailed service status info")
def service_status(verbose: bool) -> None:  # noqa: D401
    """Show current status (running/loaded). Pass -v for system output (Linux: system service only)."""
    from .service import get_manager

    mgr = get_manager(system_mode=True)
    try:
        st = mgr.status()
        color = "green" if st in {"active", "running"} else "red" if st in {"failed", "inactive"} else "yellow"
        click.echo(click.style(f"Service status: {st}", fg=color))
        if verbose and hasattr(mgr, "status_verbose"):
            click.echo("\n--- system output ---")
            click.echo(mgr.status_verbose())
    except Exception as exc:
        click.echo(click.style(f"Failed: {exc}", fg="red")) 


def _restart_service_manager() -> None:
    """Stop then start the installed Portacode system service."""
    from .restart import restart_service

    restart_service(system_mode=True)


def _restart_service_and_report(message: str = "Restarting Portacode service…") -> None:
    """Helper used by CLI commands to restart and surface friendly output."""
    click.echo(message)
    try:
        _restart_service_manager()
    except Exception as exc:
        raise click.ClickException(f"Restart failed: {exc}") from exc
    try:
        from .service import get_manager

        st = get_manager(system_mode=True).status()
    except Exception:
        st = "unknown"
    click.echo(click.style(f"✔ Service restarted (status: {st})", fg="green"))


def _exit_for_systemd_restart(message: str = "Restarting Portacode service…") -> None:
    """Ask the service manager to restart by exiting with a non-zero code."""
    from .restart import request_restart

    click.echo(message)
    # This intentionally exits with a non-zero code so a supervisor (e.g. systemd)
    # can restart the process.
    request_restart(method="exit", in_service=True)


@cli.command("restart")
@click.option(
    "--method",
    type=click.Choice(["auto", "systemctl", "exit"], case_sensitive=False),
    default="auto",
    show_default=True,
    help=(
        "How to restart. "
        "'exit' asks the supervisor (systemd) to restart by exiting 42; "
        "'systemctl' runs a real service restart; "
        "'auto' uses 'exit' when running under systemd and otherwise uses 'systemctl'."
    ),
)
@click.option("--verbose", "-v", is_flag=True, help="Print detailed service status/logs on failure.")
def restart_command(method: str, verbose: bool) -> None:
    """Restart the installed Portacode system service."""
    method = method.lower()
    try:
        from .restart import request_restart, running_under_systemd
        from .service import get_manager

        in_service = running_under_systemd() and not (sys.stdin.isatty() or sys.stdout.isatty())
        click.echo("Restarting Portacode service…")
        request_restart(method=method, in_service=in_service)
        st = get_manager(system_mode=True).status()
        click.echo(click.style(f"✔ Service restarted (status: {st})", fg="green"))
    except SystemExit:
        raise
    except Exception:
        if verbose:
            try:
                from .service import get_manager

                mgr = get_manager(system_mode=True)
                click.echo("\n--- diagnostics ---")
                if hasattr(mgr, "status_verbose"):
                    click.echo(mgr.status_verbose())
            except Exception:
                pass
        raise


@cli.command("setversion")
@click.argument("version", required=True)
def set_version_command(version: str) -> None:
    """Install a specific Portacode version and restart the system service."""
    click.echo(f"Installing Portacode {version}…")
    interactive_tty = sys.stdin.isatty() and sys.stdout.isatty()
    if interactive_tty and os.geteuid() != 0:
        click.echo(
            click.style(
                "[sudo] If required, Portacode will attempt passwordless sudo first and then prompt for your password.",
                fg="yellow",
            )
        )
    result = run_pip_install_command(
        build_pip_install_command(version=version),
        allow_sudo_fallback=True,
        interactive_sudo=interactive_tty,
    )
    if result.returncode != 0:
        error_msg = (result.stderr or result.stdout or "").strip() or "unknown error"
        raise click.ClickException(f"Failed to install Portacode {version}: {error_msg}")
    click.echo(click.style(f"✔ Portacode {version} installed", fg="green"))
    from .restart import request_restart, running_under_systemd

    in_service = running_under_systemd() and not (sys.stdin.isatty() or sys.stdout.isatty())
    click.echo("Restarting Portacode service…")
    request_restart(method="auto", in_service=in_service)
