from __future__ import annotations

import asyncio
import json
import platform
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Annotated, TypedDict

import typer
import uvicorn
from bleak.exc import BleakError
from rich.console import Console
from rich.table import Table

from k1link import __version__
from k1link.artifacts import write_json_atomic
from k1link.compute import (
    prepare_camera_compute_job,
    publish_e21_lab_instance,
    publish_e22_lab_instance,
    publish_e23_lab_instance,
    publish_e24_lab_instance,
    publish_e25_lab_instance,
    publish_e26_lab_instance,
    publish_integrated_lab_instance,
)
from k1link.device_plugins.xgrids_k1.analyze import (
    DEFAULT_STREAM_SUMMARY_MAX_PAYLOAD_BYTES,
    MAX_STREAM_SUMMARY_PAYLOAD_BYTES,
    CalibratedOverlayExperimentError,
    run_calibrated_overlay_experiment,
    summarize_mqtt_streams,
)
from k1link.device_plugins.xgrids_k1.archive import discover_legacy_viewer_sessions
from k1link.device_plugins.xgrids_k1.ble.gatt import dump_metadata
from k1link.device_plugins.xgrids_k1.ble.scanner import scan
from k1link.device_plugins.xgrids_k1.ble.wifi_provisioning import (
    PROFILE_ID,
    WriteMode,
    provision_wifi_once,
)
from k1link.device_plugins.xgrids_k1.firmware_credential import (
    FirmwareCredentialError,
    import_k1_fw302_ap_credential,
)
from k1link.device_plugins.xgrids_k1.mqtt import (
    DEFAULT_MAX_MESSAGE_BYTES,
    MAX_CONFIGURABLE_MESSAGE_BYTES,
    CaptureError,
    capture_mqtt,
)
from k1link.device_plugins.xgrids_k1.net.snapshot import snapshot
from k1link.device_plugins.xgrids_k1.protocol.application_authority import (
    ApplicationAuthorityLoadError,
    MacOSKeychainApplicationAuthorityProvisioner,
)
from k1link.device_plugins.xgrids_k1.usb.snapshot import snapshot as usb_snapshot
from k1link.macos_credentials import CredentialDialogError, prompt_wifi_credentials
from k1link.sessions import SessionIntegrityError

app = typer.Typer(
    name="k1link",
    help="Safe, evidence-led research tooling for an owner-controlled XGRIDS K1.",
    no_args_is_help=True,
)
console = Console()
ble_app = typer.Typer(help="Bluetooth LE discovery and metadata commands.", no_args_is_help=True)
net_app = typer.Typer(help="Read-only local network observation commands.", no_args_is_help=True)
usb_app = typer.Typer(help="Read-only macOS USB metadata commands.", no_args_is_help=True)
analyze_app = typer.Typer(help="Bounded offline evidence analysis commands.", no_args_is_help=True)
authority_app = typer.Typer(
    help="One-time local authority administration; never sends a K1 command.",
    no_args_is_help=True,
)
compute_app = typer.Typer(
    help="Bounded, content-addressed handoffs to external compute workers.",
    no_args_is_help=True,
)
lab_app = typer.Typer(
    help="Append-only publication of reproducible laboratory session instances.",
    no_args_is_help=True,
)
app.add_typer(ble_app, name="ble")
app.add_typer(net_app, name="net")
app.add_typer(usb_app, name="usb")
app.add_typer(analyze_app, name="analyze")
app.add_typer(authority_app, name="authority")
app.add_typer(compute_app, name="compute")
app.add_typer(lab_app, name="lab")


class ToolStatus(TypedDict):
    name: str
    available: bool
    path: str | None


class NetworkStatus(TypedDict):
    wifi_interface: str | None
    default_route_interface: str | None
    vpn_default_route: bool


class DoctorPayload(TypedDict):
    k1link_version: str
    python_version: str
    python_executable: str
    python_is_3_12: bool
    local_venv: bool
    platform: str
    machine: str
    network: NetworkStatus
    tools: list[ToolStatus]
    notes: list[str]


def _tool_status(name: str) -> ToolStatus:
    path = shutil.which(name)
    return {"name": name, "available": path is not None, "path": path}


def _command_output(args: list[str]) -> str | None:
    try:
        result = subprocess.run(
            args,
            check=False,
            capture_output=True,
            text=True,
            timeout=5,
        )
    except (OSError, subprocess.SubprocessError):
        return None
    if result.returncode != 0:
        return None
    return result.stdout.strip()


@authority_app.command("provision")
def authority_provision(
    confirm_reviewed_authority: Annotated[
        bool,
        typer.Option(
            "--confirm-reviewed-authority",
            help="Confirm this is the reviewed private LixelGO FW 3.0.2 authority.",
        ),
    ] = False,
) -> None:
    """Prompt in macOS Keychain without receiving or printing the authority."""

    if not confirm_reviewed_authority:
        console.print(
            "[red]Authority provisioning not confirmed.[/red] "
            "Add --confirm-reviewed-authority only for the reviewed private value."
        )
        raise typer.Exit(code=2)
    console.print(
        "macOS Keychain will prompt for the reviewed authority. Mission Core will not "
        "receive it through argv, environment, a file, browser state or logs."
    )
    try:
        source = MacOSKeychainApplicationAuthorityProvisioner().provision_interactively()
    except ApplicationAuthorityLoadError as exc:
        console.print(f"[red]Authority provisioning failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    console.print(
        f"[green]Keychain item validated.[/green] service={source.service!r}; "
        f"account={source.account!r}. No K1 command was sent."
    )


@authority_app.command("import-k1-fw302-ap")
def import_k1_fw302_ap(
    firmware: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Official XGRIDS K1 3.0.2 full firmware archive.",
        ),
    ],
    confirm_reviewed_firmware: Annotated[
        bool,
        typer.Option(
            "--confirm-reviewed-firmware",
            help="Confirm offline import from the exact reviewed official artifact.",
        ),
    ] = False,
) -> None:
    """Install the exact K1 AP material in the OS credential store."""

    if not confirm_reviewed_firmware:
        console.print(
            "[red]Firmware credential import not confirmed.[/red] "
            "Add --confirm-reviewed-firmware for the reviewed official 3.0.2 image."
        )
        raise typer.Exit(code=2)
    helper = (
        Path(__file__).resolve().parents[4]
        / "plugins"
        / "xgrids-k1"
        / "macos"
        / "associate_wifi.swift"
    )
    try:
        result = import_k1_fw302_ap_credential(firmware, helper)
    except (FirmwareCredentialError, OSError, ValueError) as exc:
        console.print(f"[red]Firmware credential import failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    console.print(
        "[green]Exact firmware credential provider installed.[/green] "
        f"provider={result.provider_id!r}; adapter={result.host_adapter!r}; "
        "secret_exposed=false; no K1 command was sent."
    )


def _default_route_interface() -> str | None:
    output = _command_output(["route", "-n", "get", "default"])
    if output is None:
        return None
    for line in output.splitlines():
        key, separator, value = line.strip().partition(":")
        if separator and key == "interface":
            return value.strip()
    return None


def _wifi_interface() -> str | None:
    output = _command_output(["networksetup", "-listallhardwareports"])
    if output is None:
        return None
    blocks = output.split("\n\n")
    for block in blocks:
        if "Hardware Port: Wi-Fi" not in block:
            continue
        for line in block.splitlines():
            key, separator, value = line.partition(":")
            if separator and key.strip() == "Device":
                return value.strip()
    return None


def _doctor_payload() -> DoctorPayload:
    executable = Path(sys.executable)
    default_route = _default_route_interface()
    wifi_interface = _wifi_interface()
    notes = [
        "Bluetooth permission is intentionally not requested by doctor.",
        "Missing tshark/nmap is acceptable before the network-analysis gate.",
        "No Homebrew or system changes are performed by this command.",
    ]
    if default_route is not None and default_route.startswith("utun"):
        notes.append(
            "Default route uses a VPN/tunnel interface; future K1 commands must resolve "
            "the route for the confirmed K1 IP instead of assuming the default route."
        )
    return {
        "k1link_version": __version__,
        "python_version": platform.python_version(),
        "python_executable": str(executable),
        "python_is_3_12": sys.version_info[:2] == (3, 12),
        "local_venv": Path(sys.prefix).name == ".venv",
        "platform": platform.platform(),
        "machine": platform.machine(),
        "network": {
            "wifi_interface": wifi_interface,
            "default_route_interface": default_route,
            "vpn_default_route": bool(default_route and default_route.startswith("utun")),
        },
        "tools": [_tool_status(name) for name in ("uv", "tcpdump", "tshark", "nmap", "ffmpeg")],
        "notes": notes,
    }


@app.callback()
def main() -> None:
    """Run safe research commands for an owner-controlled XGRIDS K1."""


@app.command()
def doctor(
    json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
) -> None:
    """Inspect the local toolchain without touching the K1 or system configuration."""
    payload = _doctor_payload()
    if json_output:
        typer.echo(json.dumps(payload, ensure_ascii=False, indent=2))
        return

    console.print(f"k1link {payload['k1link_version']}")
    console.print(f"Python {payload['python_version']} ({payload['python_executable']})")
    console.print(
        "Local .venv: " + ("[green]yes[/green]" if payload["local_venv"] else "[red]no[/red]")
    )
    network = payload["network"]
    console.print(
        "Wi-Fi interface: "
        f"{network['wifi_interface'] or '-'}; default route: "
        f"{network['default_route_interface'] or '-'}"
    )

    table = Table(title="External tools")
    table.add_column("Tool")
    table.add_column("Available")
    table.add_column("Path")
    for item in payload["tools"]:
        table.add_row(
            str(item["name"]),
            "yes" if item["available"] else "no",
            str(item["path"] or "-"),
        )
    console.print(table)
    for note in payload["notes"]:
        console.print(f"- {note}")


@lab_app.command("publish-integrated")
def publish_integrated_lab(
    result: Annotated[
        Path,
        typer.Option(
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted e10-integrated-perception result directory.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E19'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Publish one exact accepted visual run without copying raw sensor bytes."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_integrated_lab_instance(
            repository_root=repository_root,
            source_result_root=result,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, ValueError) as exc:
        console.print(f"[red]LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    console.print(
        "[green]LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        "raw_sensor_payloads_copied=false"
    )


@lab_app.command("publish-e21")
def publish_e21_lab(
    result: Annotated[
        Path,
        typer.Option(
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted E21 realtime-envelope result directory.",
        ),
    ],
    worker_result: Annotated[
        Path,
        typer.Option(
            "--worker-result",
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Exact E15 shadow-worker result referenced by E21.",
        ),
    ],
    semantic_reference: Annotated[
        Path,
        typer.Option(
            "--semantic-reference",
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted E10 result containing the SHA-matched semantic masks.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E21'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Publish the exact accepted E21 envelope as a visual 60-second LAB run."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_e21_lab_instance(
            repository_root=repository_root,
            e21_result_root=result,
            worker_result_root=worker_result,
            semantic_reference_result_root=semantic_reference,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, ValueError) as exc:
        console.print(f"[red]E21 LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    console.print(
        "[green]E21 LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        "source_payloads_mutated=false"
    )


@lab_app.command("publish-e22")
def publish_e22_lab(
    result: Annotated[
        Path,
        typer.Option(
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted integrated result used as the immutable E22 baseline.",
        ),
    ],
    profile: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Bounded E22 temporal-stability profile.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E22'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Build and publish a bounded 2D/3D/semantic temporal-stability run."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_e22_lab_instance(
            repository_root=repository_root,
            source_result_root=result,
            profile_path=profile,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, ValueError) as exc:
        console.print(f"[red]E22 LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    reductions = published.build.report["metrics"]["reductions"]
    console.print(
        "[green]E22 LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        f"2d_reduction={reductions['tracking_2d_acceleration_p95_fraction']:.3f}; "
        f"3d_center_reduction={reductions['cuboid_center_step_p95_fraction']:.3f}; "
        f"3d_yaw_reduction={reductions['cuboid_yaw_step_p95_fraction']:.3f}; "
        "source_payloads_mutated=false"
    )


@lab_app.command("publish-e23")
def publish_e23_lab(
    reference: Annotated[
        Path,
        typer.Option(
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted zero-based E10 result containing exact source masks.",
        ),
    ],
    worker_result: Annotated[
        Path,
        typer.Option(
            "--worker-result",
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted E23 inline-temporal worker result.",
        ),
    ],
    source_report: Annotated[
        Path,
        typer.Option(
            "--source-report",
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Exact E23 1x replay source report.",
        ),
    ],
    profile: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Bounded E23 inline-temporal profile.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E23.2'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Publish an accepted inline-temporal 1x run as an immutable LAB replay."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_e23_lab_instance(
            repository_root=repository_root,
            reference_result_root=reference,
            worker_result_root=worker_result,
            source_report_path=source_report,
            profile_path=profile,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
        console.print(f"[red]E23 LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    console.print(
        "[green]E23 LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        "speed=1.0; source_payloads_mutated=false"
    )


@lab_app.command("publish-e24")
def publish_e24_lab(
    result: Annotated[
        Path,
        typer.Option(
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted full-session integrated result used as the E24 source.",
        ),
    ],
    profile: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Bounded E24 world-motion profile.",
        ),
    ],
    benchmark: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Operator-anchored E24 motion benchmark.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E24'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Build and publish a bounded world-frame motion-tracking run."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_e24_lab_instance(
            repository_root=repository_root,
            source_result_root=result,
            profile_path=profile,
            benchmark_path=benchmark,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
        console.print(f"[red]E24 LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    metrics = published.build.report["metrics"]
    console.print(
        "[green]E24 LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        f"benchmark={metrics['benchmark']['passed_events']}/"
        f"{metrics['benchmark']['total_events']}; "
        f"p95={metrics['runtime']['world_motion_frame_processing_ms']['p95']:.3f}ms; "
        "source_payloads_mutated=false"
    )


@lab_app.command("publish-e25")
def publish_e25_lab(
    result: Annotated[
        Path,
        typer.Option(
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted full-session integrated result used as the E25 source.",
        ),
    ],
    world_profile: Annotated[
        Path,
        typer.Option(
            "--world-profile",
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Bounded E24 world-association profile used before E25 evidence.",
        ),
    ],
    profile: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Bounded E25 persistent-support profile.",
        ),
    ],
    benchmark: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Spatially bound E25 motion benchmark.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E25.1'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Build and publish map-frame persistent-support motion evidence."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_e25_lab_instance(
            repository_root=repository_root,
            source_result_root=result,
            world_motion_profile_path=world_profile,
            profile_path=profile,
            benchmark_path=benchmark,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
        console.print(f"[red]E25 LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    metrics = published.build.report["metrics"]
    console.print(
        "[green]E25 LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        f"benchmark={metrics['benchmark']['passed_events']}/"
        f"{metrics['benchmark']['total_events']}; "
        f"p95={metrics['runtime']['persistent_support_frame_processing_ms']['p95']:.3f}ms; "
        "source_payloads_mutated=false"
    )


@lab_app.command("publish-e26")
def publish_e26_lab(
    lidar_result: Annotated[
        Path,
        typer.Option(
            "--lidar-result",
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted E25 persistent-support result.",
        ),
    ],
    camera_result: Annotated[
        Path,
        typer.Option(
            "--camera-result",
            exists=True,
            file_okay=False,
            readable=True,
            resolve_path=True,
            help="Accepted pre-world-motion result containing every 2D track.",
        ),
    ],
    profile: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Bounded E26 camera/ego-motion profile.",
        ),
    ],
    benchmark: Annotated[
        Path,
        typer.Option(
            exists=True,
            dir_okay=False,
            readable=True,
            resolve_path=True,
            help="Spatially bound E26 motion benchmark.",
        ),
    ],
    session_id: Annotated[
        str,
        typer.Option("--session-id", help="New immutable LAB session id."),
    ],
    lab_id: Annotated[
        str,
        typer.Option("--lab-id", help="LAB marker, for example 'LAB E26.1'."),
    ],
    display_name: Annotated[
        str,
        typer.Option("--display-name", help="Operator-facing saved-session title."),
    ],
) -> None:
    """Build and publish calibrated camera/ego-motion evidence."""

    repository_root = Path(__file__).resolve().parents[4]
    try:
        published = publish_e26_lab_instance(
            repository_root=repository_root,
            lidar_result_root=lidar_result,
            camera_result_root=camera_result,
            profile_path=profile,
            benchmark_path=benchmark,
            lab_session_id=session_id,
            lab_id=lab_id,
            display_name=display_name,
        )
    except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
        console.print(f"[red]E26 LAB publication failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc
    metrics = published.build.report["metrics"]
    console.print(
        "[green]E26 LAB instance published.[/green] "
        f"session={published.binding.session_id}; "
        f"source={published.binding.source_session_id}; "
        f"result={published.binding.result_id}; "
        f"benchmark={metrics['benchmark']['passed_events']}/"
        f"{metrics['benchmark']['total_events']}; "
        f"p95={metrics['runtime']['camera_ego_motion_frame_processing_ms']['p95']:.3f}ms; "
        "camera_metric_velocity=false; source_payloads_mutated=false"
    )


@app.command("serve")
def serve_console(
    port: Annotated[
        int,
        typer.Option(min=1024, max=65535, help="Loopback HTTP port for the local console."),
    ] = 8000,
) -> None:
    """Serve the built Mission Core Control Station and loopback control API."""
    frontend = Path(__file__).resolve().parents[4] / "apps" / "control-station" / "dist"
    if not frontend.is_dir():
        console.print(
            "[red]Frontend build is missing.[/red] Run npm install && npm run build "
            "inside apps/control-station."
        )
        raise typer.Exit(code=2)
    console.print(f"NODEDC MISSION CORE: http://127.0.0.1:{port}")
    console.print("The credential endpoint is bound to this Mac only.")
    uvicorn.run(
        "k1link.web.app:app",
        host="127.0.0.1",
        port=port,
        log_level="info",
        access_log=True,
    )


@ble_app.command("scan")
def ble_scan(
    out: Annotated[
        Path,
        typer.Option(help="Ignored session JSON path; parent directories are created."),
    ],
    duration: Annotated[
        float,
        typer.Option(min=1.0, max=300.0, help="Scan duration in seconds."),
    ] = 30.0,
) -> None:
    """Discover BLE advertisements without connecting or changing device configuration."""
    try:
        result = asyncio.run(scan(duration))
    except (BleakError, OSError, ValueError) as exc:
        console.print(f"[red]BLE scan failed:[/red] {type(exc).__name__}: {exc}")
        console.print("Check System Settings → Privacy & Security → Bluetooth.")
        raise typer.Exit(code=2) from exc

    write_json_atomic(out, result)
    table = Table(title=f"BLE devices ({result['device_count']})")
    table.add_column("Candidate")
    table.add_column("Name")
    table.add_column("RSSI")
    table.add_column("macOS UUID")
    for device in result["devices"]:
        table.add_row(
            "K1?" if device["k1_name_candidate"] else "",
            device["local_name"] or device["name"] or "-",
            str(device["rssi"]),
            device["macos_uuid"],
        )
    console.print(table)
    console.print(f"Saved: {out}")


@ble_app.command("gatt-dump")
def ble_gatt_dump(
    device: Annotated[str, typer.Option(help="CoreBluetooth/macOS UUID from ble scan.")],
    out: Annotated[
        Path,
        typer.Option(help="Ignored session JSON path; parent directories are created."),
    ],
    timeout: Annotated[
        float,
        typer.Option(min=5.0, max=120.0, help="Connection timeout in seconds."),
    ] = 45.0,
) -> None:
    """Enumerate GATT metadata only: no characteristic reads, subscriptions or writes."""
    console.print(
        "Connecting for service discovery only; no characteristic values will be read or written."
    )
    try:
        result = asyncio.run(dump_metadata(device, timeout))
    except (BleakError, OSError, ValueError) as exc:
        console.print(f"[red]GATT metadata failed:[/red] {type(exc).__name__}: {exc}")
        raise typer.Exit(code=2) from exc

    write_json_atomic(out, result)
    characteristic_count = sum(len(service["characteristics"]) for service in result["services"])
    console.print(
        f"Device: {result['device_name']}; services: {len(result['services'])}; "
        f"characteristics: {characteristic_count}"
    )
    console.print(f"Saved: {out}")


@ble_app.command("wifi-configure")
def ble_wifi_configure(
    device: Annotated[str, typer.Option(help="CoreBluetooth/macOS UUID from ble scan.")],
    out: Annotated[
        Path,
        typer.Option(help="Ignored sensitive session JSON path; parent directories are created."),
    ],
    profile: Annotated[
        str,
        typer.Option(help="Exact reviewed provisioning profile ID."),
    ],
    confirm_write: Annotated[
        bool,
        typer.Option(
            "--confirm-write",
            help="Confirm one state-changing BLE Wi-Fi provisioning write.",
        ),
    ] = False,
    write_mode: Annotated[
        WriteMode,
        typer.Option(help="ATT write mode; auto follows the live characteristic properties."),
    ] = "auto",
    timeout: Annotated[
        float,
        typer.Option(min=10.0, max=120.0, help="Status polling timeout after the write."),
    ] = 45.0,
) -> None:
    """Send router credentials once using the reviewed K1 firmware-3 profile."""
    if profile != PROFILE_ID:
        console.print(f"[red]Unknown or unreviewed profile:[/red] {profile}")
        raise typer.Exit(code=2)
    if not confirm_write:
        console.print(
            "[red]Write not confirmed.[/red] Add --confirm-write after reviewing the profile."
        )
        raise typer.Exit(code=2)

    console.print(
        "Two local macOS dialogs will request the Wi-Fi name and hidden password. "
        "The password is never printed, logged, or written to the result file; "
        "the K1 may echo the SSID in the ignored sensitive status result."
    )
    try:
        ssid, password = prompt_wifi_credentials()
    except CredentialDialogError as exc:
        console.print(f"[red]Credential entry failed:[/red] {exc}")
        raise typer.Exit(code=2) from exc

    console.print("Credentials accepted locally. Starting the single reviewed BLE write.")
    try:
        result = asyncio.run(
            provision_wifi_once(
                device,
                ssid,
                password,
                timeout_seconds=timeout,
                write_mode=write_mode,
            )
        )
    except (BleakError, OSError, TimeoutError, ValueError) as exc:
        console.print(f"[red]Wi-Fi provisioning failed:[/red] {type(exc).__name__}: {exc}")
        console.print("No automatic retry was attempted.")
        raise typer.Exit(code=2) from exc
    finally:
        password = ""
        ssid = ""

    write_json_atomic(out, result)
    observations = result["observations"]
    final_status = observations[-1]["status"] if observations else result["baseline_status"]
    console.print(f"Outcome: {result['outcome']}; ATT mode: {result['write_mode']}")
    console.print(
        f"Final status code: {final_status['status_code']}; "
        f"reported IPv4: {final_status['ipv4'] or '-'}"
    )
    console.print("Saved sensitive device/network metadata; do not commit the output.")
    console.print(f"Saved: {out}")


@net_app.command("snapshot")
def net_snapshot(
    out: Annotated[
        Path,
        typer.Option(help="Ignored session JSON path; parent directories are created."),
    ],
) -> None:
    """Save routes, interfaces and the existing neighbor table without scanning the LAN."""
    result = snapshot()
    write_json_atomic(out, result)
    console.print("Saved a sensitive local network snapshot; do not commit the output.")
    console.print(f"Saved: {out}")


@net_app.command("mqtt-capture")
def net_mqtt_capture(
    host: Annotated[
        str,
        typer.Option("--host", help="Confirmed K1 RFC1918 IPv4 address; hostnames are rejected."),
    ],
    out: Annotated[
        Path,
        typer.Option(
            "--out",
            help="Sensitive output directory; use captures/... so Git ignores it.",
        ),
    ],
    confirm_owned_device: Annotated[
        bool,
        typer.Option(
            "--confirm-owned-device",
            help="Confirm the target is an owner-controlled K1 before connecting.",
        ),
    ] = False,
    port: Annotated[
        int,
        typer.Option(min=1, max=65535, help="MQTT broker TCP port."),
    ] = 1883,
    duration: Annotated[
        float,
        typer.Option(min=1.0, help="Capture duration after SUBACK, in seconds."),
    ] = 60.0,
    max_message_bytes: Annotated[
        int,
        typer.Option(
            min=1,
            max=MAX_CONFIGURABLE_MESSAGE_BYTES,
            help="Abort before storing a payload larger than this byte limit.",
        ),
    ] = DEFAULT_MAX_MESSAGE_BYTES,
) -> None:
    """Capture fixed K1 MQTT report topics once; never publish or reconnect."""
    if not confirm_owned_device:
        console.print(
            "[red]Target ownership not confirmed.[/red] "
            "Add --confirm-owned-device for the confirmed K1 IPv4 address."
        )
        raise typer.Exit(code=2)

    console.print(
        "Starting one read-only MQTT subscription session. "
        "No application messages will be published and no reconnect will be attempted."
    )
    try:
        result = capture_mqtt(
            host,
            out,
            port=port,
            duration_seconds=duration,
            max_message_bytes=max_message_bytes,
            on_ready=lambda: console.print(
                "[green]MQTT subscriptions active; capture timer started.[/green]"
            ),
        )
    except CaptureError as exc:
        console.print(f"[red]MQTT capture failed:[/red] {exc}")
        if exc.summary is not None:
            console.print(f"Partial artifacts preserved in: {out}")
        raise typer.Exit(code=2) from exc
    except (OSError, RuntimeError, ValueError) as exc:
        console.print(f"[red]MQTT capture failed:[/red] {type(exc).__name__}: {exc}")
        raise typer.Exit(code=2) from exc

    console.print(
        f"Capture stopped: {result['stop_reason']}; messages: {result['message_count']}; "
        f"payload bytes: {result['payload_bytes']}"
    )
    console.print("Saved sensitive raw MQTT evidence; do not commit the output.")
    console.print(f"Saved: {out}")


@usb_app.command("snapshot")
def usb_snapshot_command(
    out: Annotated[
        Path,
        typer.Option(help="Ignored session JSON path; parent directories are created."),
    ],
) -> None:
    """Save XGRIDS USB/interface/storage metadata without opening device files."""
    result = usb_snapshot()
    write_json_atomic(out, result)
    console.print(
        "Saved sensitive USB metadata only; no sudo, device-file reads or device writes used."
    )
    console.print(f"XGRIDS candidates: {result['xgrids_device_count']}")
    console.print(f"Saved: {out}")


@analyze_app.command("mqtt-streams")
def analyze_mqtt_streams(
    capture: Annotated[
        Path,
        typer.Option(
            "--capture",
            exists=True,
            file_okay=True,
            dir_okay=False,
            readable=True,
            help="Repository-native mqtt.raw.k1mqtt capture to analyze offline.",
        ),
    ],
    out: Annotated[
        Path,
        typer.Option(
            "--out",
            help=(
                "Sensitive atomic JSON output; keep under captures/, sessions/, or "
                "artifacts/decoded/."
            ),
        ),
    ],
    max_payload_bytes: Annotated[
        int,
        typer.Option(
            "--max-payload-bytes",
            min=1,
            max=MAX_STREAM_SUMMARY_PAYLOAD_BYTES,
            help="Reject a capture frame larger than this bounded payload limit.",
        ),
    ] = DEFAULT_STREAM_SUMMARY_MAX_PAYLOAD_BYTES,
) -> None:
    """Summarize captured firmware-3 point-cloud and pose streams without coordinates."""
    if capture.expanduser().resolve() == out.expanduser().resolve():
        console.print("[red]Analysis failed:[/red] capture and output must be different files")
        raise typer.Exit(code=2)

    try:
        result = summarize_mqtt_streams(
            capture,
            max_payload_bytes=max_payload_bytes,
        )
        write_json_atomic(out, result)
    except (OSError, RuntimeError, ValueError) as exc:
        console.print(f"[red]Analysis failed:[/red] {type(exc).__name__}: {exc}")
        raise typer.Exit(code=2) from exc

    console.print(
        f"Frames: {result['frames']['count']}; decode successes: "
        f"{result['decoding']['successes']}; errors: {result['decoding']['errors']}"
    )
    console.print("Saved sensitive aggregate-only output; keep it in ignored storage.")
    console.print(f"Saved: {out}")


@analyze_app.command("calibrated-overlay")
def analyze_calibrated_overlay(
    session_root: Annotated[
        Path,
        typer.Option(
            "--session-root",
            exists=True,
            file_okay=False,
            dir_okay=True,
            readable=True,
            resolve_path=True,
            help="Sealed observation session containing MQTT and camera evidence.",
        ),
    ],
    calibration_snapshot: Annotated[
        Path,
        typer.Option(
            "--calibration-snapshot",
            exists=True,
            file_okay=False,
            dir_okay=True,
            readable=True,
            resolve_path=True,
            help="Private physical K1 factory-calibration snapshot directory.",
        ),
    ],
    out_root: Annotated[
        Path,
        typer.Option(
            "--out-root",
            help="Mission Core evidence root; the result is sealed below private/.",
        ),
    ],
    source_id: Annotated[
        str,
        typer.Option("--source", help="Canonical archived K1 main-camera source id."),
    ] = "sensor.camera.right",
    video_offsets: Annotated[
        list[float] | None,
        typer.Option(
            "--video-offset",
            help="Repeat for each diagnostic second inside the camera epoch.",
        ),
    ] = None,
    temporal_offset_seconds: Annotated[
        float,
        typer.Option(
            "--temporal-offset",
            min=-5.0,
            max=5.0,
            help="Explicit LiDAR minus camera host-arrival offset for experiments.",
        ),
    ] = 0.0,
) -> None:
    """Build a read-only LiDAR→KB4→camera diagnostic from sealed evidence."""

    ffmpeg = shutil.which("ffmpeg")
    if ffmpeg is None:
        console.print("[red]Calibrated overlay failed:[/red] ffmpeg is unavailable")
        raise typer.Exit(code=2)
    offsets = tuple(video_offsets or (60.0, 180.0, 300.0, 420.0))
    try:
        result = run_calibrated_overlay_experiment(
            session_root=session_root,
            calibration_snapshot_root=calibration_snapshot,
            source_id=source_id,
            video_offsets_seconds=offsets,
            output_root=out_root,
            ffmpeg_path=Path(ffmpeg),
            temporal_offset_seconds=temporal_offset_seconds,
        )
    except (CalibratedOverlayExperimentError, OSError, RuntimeError, ValueError) as exc:
        console.print(f"[red]Calibrated overlay failed:[/red] {type(exc).__name__}: {exc}")
        raise typer.Exit(code=2) from exc
    console.print(
        f"[green]Calibrated diagnostic ready:[/green] {result.experiment_id}; "
        f"frames: {result.frame_count}; source: {result.source_id}"
    )
    console.print(f"Calibration: {result.calibration_content_identity}")
    console.print(f"Rerun: {result.rerun_path}")
    console.print(f"Mosaic: {result.mosaic_path}")
    console.print("P0 remains open until static-landmark reprojection is measured.")


@compute_app.command("prepare-camera-job")
def prepare_camera_job(
    session_root: Annotated[
        Path,
        typer.Option(
            "--session-root",
            exists=True,
            file_okay=False,
            dir_okay=True,
            readable=True,
            help="Sealed observation session containing the canonical camera epoch.",
        ),
    ],
    source_id: Annotated[
        str,
        typer.Option("--source", help="Canonical archived camera source id."),
    ],
    codec_epoch: Annotated[
        int,
        typer.Option("--epoch", min=1, help="Physical camera codec epoch number."),
    ],
    out_root: Annotated[
        Path,
        typer.Option(
            "--out-root",
            help="Ignored private root where the immutable job directory is published.",
        ),
    ],
) -> None:
    """Validate and package one archived camera epoch without touching the K1."""

    session = session_root.expanduser().resolve()
    candidates = {
        candidate.session_id: candidate
        for candidate in discover_legacy_viewer_sessions(session.parent)
    }
    candidate = candidates.get(session.name)
    if candidate is None or candidate.session_root != session:
        console.print("[red]Compute job failed:[/red] session is not a sealed archive")
        raise typer.Exit(code=2)
    if all(media.source_id != source_id for media in candidate.media_sources):
        console.print("[red]Compute job failed:[/red] camera source is not cataloged")
        raise typer.Exit(code=2)
    try:
        job = prepare_camera_compute_job(
            session_root=session,
            source_id=source_id,
            codec_epoch=codec_epoch,
            origin_epoch_ns=candidate.timeline_origin_epoch_ns,
            origin_monotonic_ns=candidate.timeline_origin_monotonic_ns,
            output_root=out_root,
        )
    except (OSError, SessionIntegrityError, ValueError) as exc:
        console.print(f"[red]Compute job failed:[/red] {type(exc).__name__}: {exc}")
        raise typer.Exit(code=2) from exc

    console.print(f"[green]Compute job ready:[/green] {job.job_id}")
    console.print(
        f"Segments: {job.segment_count}; bytes: {job.input_byte_length}; "
        f"session time: {job.timeline_start_seconds:.6f}–"
        f"{job.timeline_end_seconds:.6f} s"
    )
    console.print(f"Input SHA-256: {job.input_sha256}")
    console.print(f"Saved: {job.job_root}")


if __name__ == "__main__":
    app()
