from __future__ import annotations

import argparse
import json
from pathlib import Path
import sys
import time

from govoplan_core.core.module_installer import (
    ModuleInstallerError,
    cancel_module_installer_request,
    claim_next_module_installer_request,
    default_installer_runtime_dir,
    list_module_installer_runs,
    list_module_installer_requests,
    module_installer_daemon_status,
    module_install_preflight,
    module_installer_lock_status,
    queue_module_installer_request,
    read_module_installer_run,
    read_module_installer_request,
    retry_module_installer_request,
    rollback_module_install_run,
    run_module_install_plan,
    supervise_module_install_plan,
    update_module_installer_request,
    update_module_installer_daemon_status,
)
from govoplan_core.core.module_installer_notifications import (
    build_runtime_notification_registry,
    emit_module_installer_notification,
    installer_notification_body,
    installer_notification_priority,
    installer_notification_subject,
)
from govoplan_core.core.module_license import issue_module_license, module_license_diagnostics
from govoplan_core.core.module_package_catalog import sign_module_package_catalog, validate_module_package_catalog
from govoplan_core.core.module_management import (
    configured_enabled_modules,
    saved_desired_enabled_modules,
    saved_module_install_plan,
)
from govoplan_core.db.session import configure_database, get_database
from govoplan_core.server.registry import available_module_manifests
from govoplan_core.settings import settings


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Preflight, apply, or roll back a GovOPlaN module package install plan.")
    parser.add_argument("--database-url", default=settings.database_url, help="Database URL containing system_settings.")
    parser.add_argument("--runtime-dir", type=Path, help="Directory for installer locks and run snapshots.")
    parser.add_argument("--webui-root", type=Path, default=_default_webui_root(), help="Core WebUI root for npm package changes.")
    parser.add_argument("--npm-bin", default="npm", help="npm executable to use for WebUI package changes.")
    parser.add_argument("--build-webui", action="store_true", help="Run npm run build after npm install.")
    parser.add_argument("--migrate", action="store_true", help="Run core/module database migrations after package changes and before restart.")
    parser.add_argument("--database-backup-command", help="Shell command that creates a database backup before --migrate for non-SQLite databases.")
    parser.add_argument("--database-restore-command", help="Shell command that restores the database backup during rollback.")
    parser.add_argument("--database-restore-check-command", help="Shell command that validates the created backup can be used before migrations proceed.")
    parser.add_argument("--no-activate-installed-modules", action="store_true", help="Do not add successfully installed modules to saved startup state.")
    parser.add_argument("--keep-uninstalled-modules-in-desired", action="store_true", help="Do not remove successfully uninstalled modules from saved startup state.")
    parser.add_argument("--format", choices=("shell", "json"), default="shell", help="Output format for preflight/dry-run.")
    parser.add_argument("--apply", action="store_true", help="Execute the saved plan after preflight passes.")
    parser.add_argument("--dry-run", action="store_true", help="Create a run record and snapshots but do not execute commands.")
    parser.add_argument("--supervise", action="store_true", help="Apply the plan, run optional restart/health checks, and roll back if the server does not recover.")
    parser.add_argument("--restart-command", help="Shell command run after apply and after automatic rollback, for example a systemctl restart command.")
    parser.add_argument("--health-url", help="HTTP health URL to poll after apply, for example http://127.0.0.1:8000/health.")
    parser.add_argument("--health-timeout-seconds", type=float, default=60.0, help="Maximum time to wait for health after restart.")
    parser.add_argument("--health-interval-seconds", type=float, default=2.0, help="Delay between health probes.")
    parser.add_argument("--rollback", metavar="RUN_ID", help="Restore package snapshots from a previous installer run.")
    parser.add_argument("--list-runs", action="store_true", help="List recent installer run records and lock status.")
    parser.add_argument("--show-run", metavar="RUN_ID", help="Print one installer run record.")
    parser.add_argument("--lock-status", action="store_true", help="Print the current installer lock status.")
    parser.add_argument("--list-requests", action="store_true", help="List queued/running/completed installer requests.")
    parser.add_argument("--show-request", metavar="REQUEST_ID", help="Print one installer request record.")
    parser.add_argument("--cancel-request", metavar="REQUEST_ID", help="Cancel a queued installer request.")
    parser.add_argument("--retry-request", metavar="REQUEST_ID", help="Queue a retry for a failed or cancelled installer request.")
    parser.add_argument("--enqueue-supervised", action="store_true", help="Queue a supervised installer request for a running daemon instead of applying immediately.")
    parser.add_argument("--daemon-status", action="store_true", help="Print installer daemon heartbeat/status.")
    parser.add_argument("--daemon", action="store_true", help="Run the installer request daemon.")
    parser.add_argument("--daemon-once", action="store_true", help="Process at most one queued request and exit.")
    parser.add_argument("--poll-interval-seconds", type=float, default=2.0, help="Delay between daemon queue polls.")
    parser.add_argument("--validate-package-catalog", nargs="?", const="", metavar="PATH", help="Validate a module package catalog JSON file, or the configured catalog when PATH is omitted.")
    parser.add_argument("--require-signed-catalog", action="store_true", help="Require package catalog validation to trust an Ed25519 signature.")
    parser.add_argument("--approved-catalog-channel", action="append", default=[], help="Approved package catalog channel; may be repeated.")
    parser.add_argument("--catalog-trusted-key", action="append", default=[], metavar="KEY_ID=BASE64_PUBLIC_KEY", help="Trusted Ed25519 catalog public key; may be repeated.")
    parser.add_argument("--sign-package-catalog", type=Path, metavar="PATH", help="Sign a module package catalog JSON file.")
    parser.add_argument("--catalog-signing-key-id", help="Key id to record when signing a package catalog.")
    parser.add_argument("--catalog-signing-private-key", type=Path, help="PEM Ed25519 private key used by --sign-package-catalog.")
    parser.add_argument("--catalog-output", type=Path, help="Output path for --sign-package-catalog. Defaults to overwriting the input file.")
    parser.add_argument("--validate-license", nargs="?", const="", metavar="PATH", help="Validate a license JSON file, or the configured license when PATH is omitted.")
    parser.add_argument("--require-trusted-license", action="store_true", help="Require license validation to trust an Ed25519 signature.")
    parser.add_argument("--license-trusted-key", action="append", default=[], metavar="KEY_ID=BASE64_PUBLIC_KEY", help="Trusted Ed25519 license public key; may be repeated.")
    parser.add_argument("--license-required-feature", action="append", default=[], help="License feature to check; may be repeated.")
    parser.add_argument("--issue-license", type=Path, metavar="PATH", help="Write a signed offline license JSON file.")
    parser.add_argument("--license-id", help="License id for --issue-license.")
    parser.add_argument("--license-subject", help="License subject/customer for --issue-license.")
    parser.add_argument("--license-feature", action="append", default=[], help="Feature entitlement to include in --issue-license; may be repeated.")
    parser.add_argument("--license-valid-from", help="License valid_from timestamp. Defaults to now.")
    parser.add_argument("--license-valid-until", help="License valid_until timestamp.")
    parser.add_argument("--license-signing-key-id", help="Signing key id for --issue-license.")
    parser.add_argument("--license-signing-private-key", type=Path, help="PEM Ed25519 private key for --issue-license.")
    parser.add_argument("--license-issuer", help="Optional issuer string for --issue-license.")
    parser.add_argument("--license-notes", help="Optional operator note for --issue-license.")
    return parser


def main() -> int:
    args = _build_parser().parse_args()
    runtime_dir = args.runtime_dir or default_installer_runtime_dir(args.database_url)
    try:
        return _dispatch_command(args=args, runtime_dir=runtime_dir)
    except ModuleInstallerError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1


def _dispatch_command(*, args: argparse.Namespace, runtime_dir: Path) -> int:
    for handler in (
        _handle_license_command,
        _handle_catalog_command,
        _handle_runtime_command,
        _handle_queue_command,
        _handle_run_history_command,
    ):
        result = handler(args=args, runtime_dir=runtime_dir)
        if result is not None:
            return result
    return _handle_install_command(args=args, runtime_dir=runtime_dir)


def _handle_license_command(*, args: argparse.Namespace, runtime_dir: Path) -> int | None:
    del runtime_dir
    if args.issue_license:
        return _issue_license_command(args)
    if args.validate_license is not None:
        return _validate_license_command(args)
    return None


def _issue_license_command(args: argparse.Namespace) -> int:
    if not args.license_id or not args.license_subject or not args.license_valid_until:
        raise ModuleInstallerError("--issue-license requires --license-id, --license-subject, and --license-valid-until.")
    if not args.license_signing_key_id or not args.license_signing_private_key:
        raise ModuleInstallerError("--issue-license requires --license-signing-key-id and --license-signing-private-key.")
    if not [item for item in args.license_feature if item.strip()]:
        raise ModuleInstallerError("--issue-license requires at least one --license-feature.")
    try:
        path = issue_module_license(
            path=args.issue_license,
            license_id=args.license_id,
            subject=args.license_subject,
            features=args.license_feature,
            valid_from=args.license_valid_from,
            valid_until=args.license_valid_until,
            signing_key_id=args.license_signing_key_id,
            signing_private_key_path=args.license_signing_private_key,
            issuer=args.license_issuer,
            notes=args.license_notes,
        )
    except (OSError, ValueError) as exc:
        raise ModuleInstallerError(str(exc)) from exc
    _print_result(
        {
            "issued": True,
            "path": str(path),
            "license_id": args.license_id,
            "subject": args.license_subject,
            "features": list(dict.fromkeys(item.strip() for item in args.license_feature if item.strip())),
            "valid_until": args.license_valid_until,
            "key_id": args.license_signing_key_id,
        },
        output_format=args.format,
    )
    return 0


def _validate_license_command(args: argparse.Namespace) -> int:
    path = Path(args.validate_license).expanduser() if args.validate_license else None
    result = module_license_diagnostics(
        path,
        require_trusted=args.require_trusted_license,
        trusted_keys=_trusted_keys_from_args(args.license_trusted_key, flag_name="--license-trusted-key"),
        required_features=args.license_required_feature,
    )
    _print_result(result, output_format=args.format)
    return 0 if result.get("valid") and not result.get("missing_features") else 1


def _handle_catalog_command(*, args: argparse.Namespace, runtime_dir: Path) -> int | None:
    del runtime_dir
    if args.sign_package_catalog:
        return _sign_package_catalog_command(args)
    if args.validate_package_catalog is not None:
        return _validate_package_catalog_command(args)
    return None


def _sign_package_catalog_command(args: argparse.Namespace) -> int:
    if not args.catalog_signing_key_id or not args.catalog_signing_private_key:
        raise ModuleInstallerError("--sign-package-catalog requires --catalog-signing-key-id and --catalog-signing-private-key.")
    path = sign_module_package_catalog(
        path=args.sign_package_catalog,
        key_id=args.catalog_signing_key_id,
        private_key_path=args.catalog_signing_private_key,
        output_path=args.catalog_output,
    )
    _print_result({"signed": True, "path": str(path), "key_id": args.catalog_signing_key_id}, output_format=args.format)
    return 0


def _validate_package_catalog_command(args: argparse.Namespace) -> int:
    path = Path(args.validate_package_catalog).expanduser() if args.validate_package_catalog else None
    result = validate_module_package_catalog(
        path,
        require_trusted=args.require_signed_catalog,
        approved_channels=tuple(args.approved_catalog_channel),
        trusted_keys=_trusted_keys_from_args(args.catalog_trusted_key, flag_name="--catalog-trusted-key"),
    )
    _print_result(result, output_format=args.format)
    return 0 if result.get("valid") else 1


def _handle_runtime_command(*, args: argparse.Namespace, runtime_dir: Path) -> int | None:
    if args.daemon_status:
        _print_result(module_installer_daemon_status(runtime_dir=runtime_dir), output_format=args.format)
        return 0
    if args.daemon or args.daemon_once:
        return _run_daemon(args=args, runtime_dir=runtime_dir)
    return None


def _handle_queue_command(*, args: argparse.Namespace, runtime_dir: Path) -> int | None:
    if args.list_requests:
        _print_result({
            "requests": list(list_module_installer_requests(runtime_dir=runtime_dir)),
            "daemon": module_installer_daemon_status(runtime_dir=runtime_dir),
        }, output_format=args.format)
        return 0
    if args.show_request:
        _print_result(read_module_installer_request(runtime_dir=runtime_dir, request_id=args.show_request), output_format=args.format)
        return 0
    if args.cancel_request:
        _print_result(cancel_module_installer_request(runtime_dir=runtime_dir, request_id=args.cancel_request, cancelled_by="cli"), output_format=args.format)
        return 0
    if args.retry_request:
        _print_result(retry_module_installer_request(runtime_dir=runtime_dir, request_id=args.retry_request, requested_by="cli"), output_format=args.format)
        return 0
    if args.enqueue_supervised:
        request = queue_module_installer_request(runtime_dir=runtime_dir, requested_by="cli", options=_request_options_from_args(args))
        _print_result(request, output_format=args.format)
        return 0
    return None


def _handle_run_history_command(*, args: argparse.Namespace, runtime_dir: Path) -> int | None:
    if args.list_runs:
        _print_result({
            "runs": list(list_module_installer_runs(runtime_dir=runtime_dir)),
            "lock": module_installer_lock_status(runtime_dir=runtime_dir),
        }, output_format=args.format)
        return 0
    if args.show_run:
        _print_result(read_module_installer_run(runtime_dir=runtime_dir, run_id=args.show_run), output_format=args.format)
        return 0
    if args.lock_status:
        _print_result(module_installer_lock_status(runtime_dir=runtime_dir), output_format=args.format)
        return 0
    if args.rollback:
        return _rollback_command(args=args, runtime_dir=runtime_dir)
    return None


def _rollback_command(*, args: argparse.Namespace, runtime_dir: Path) -> int:
    result = rollback_module_install_run(
        run_id=args.rollback,
        runtime_dir=runtime_dir,
        npm_bin=args.npm_bin,
        build_webui=args.build_webui,
        database_restore_command=args.database_restore_command,
        database_url=str(args.database_url),
    )
    _print_result(result.as_dict(), output_format=args.format)
    return 0 if result.return_code == 0 else 1


def _handle_install_command(*, args: argparse.Namespace, runtime_dir: Path) -> int:
    configure_database(str(args.database_url))
    available = available_module_manifests(ignore_load_errors=True)
    with get_database().session() as session:
        configured = configured_enabled_modules(settings.enabled_modules)
        desired = saved_desired_enabled_modules(session, configured)
        plan = saved_module_install_plan(session)
        if args.supervise:
            return _supervise_install_command(args=args, runtime_dir=runtime_dir, session=session, available=available, desired=desired, plan=plan)
        if args.apply or args.dry_run:
            return _apply_install_command(args=args, runtime_dir=runtime_dir, session=session, available=available, desired=desired, plan=plan)
        return _preflight_install_command(args=args, runtime_dir=runtime_dir, session=session, available=available, desired=desired, plan=plan)


def _supervise_install_command(
    *,
    args: argparse.Namespace,
    runtime_dir: Path,
    session: object,
    available: object,
    desired: object,
    plan: object,
) -> int:
    result = supervise_module_install_plan(
        session=session,
        plan=plan,
        available=available,
        current_enabled=desired,
        desired_enabled=desired,
        database_url=str(args.database_url),
        runtime_dir=runtime_dir,
        webui_root=args.webui_root,
        npm_bin=args.npm_bin,
        build_webui=args.build_webui,
        migrate_database=args.migrate,
        database_backup_command=args.database_backup_command,
        database_restore_command=args.database_restore_command,
        database_restore_check_command=args.database_restore_check_command,
        activate_installed_modules=not args.no_activate_installed_modules,
        remove_uninstalled_modules_from_desired=not args.keep_uninstalled_modules_in_desired,
        restart_command=args.restart_command,
        health_url=args.health_url,
        health_timeout_seconds=args.health_timeout_seconds,
        health_interval_seconds=args.health_interval_seconds,
    )
    _print_result(result.as_dict(), output_format=args.format)
    return 0 if result.return_code == 0 else 1


def _apply_install_command(
    *,
    args: argparse.Namespace,
    runtime_dir: Path,
    session: object,
    available: object,
    desired: object,
    plan: object,
) -> int:
    result = run_module_install_plan(
        session=session,
        plan=plan,
        available=available,
        current_enabled=desired,
        desired_enabled=desired,
        database_url=str(args.database_url),
        runtime_dir=runtime_dir,
        webui_root=args.webui_root,
        npm_bin=args.npm_bin,
        build_webui=args.build_webui,
        migrate_database=args.migrate,
        database_backup_command=args.database_backup_command,
        database_restore_command=args.database_restore_command,
        database_restore_check_command=args.database_restore_check_command,
        activate_installed_modules=not args.no_activate_installed_modules,
        remove_uninstalled_modules_from_desired=not args.keep_uninstalled_modules_in_desired,
        dry_run=args.dry_run,
    )
    _print_result(result.as_dict(), output_format=args.format)
    return 0 if result.return_code == 0 else 1


def _preflight_install_command(
    *,
    args: argparse.Namespace,
    runtime_dir: Path,
    session: object,
    available: object,
    desired: object,
    plan: object,
) -> int:
    from govoplan_core.core.maintenance import saved_maintenance_mode

    maintenance = saved_maintenance_mode(session)
    preflight = module_install_preflight(
        plan=plan,
        available=available,
        current_enabled=desired,
        desired_enabled=desired,
        maintenance_mode=maintenance.enabled,
        session=session,
        webui_root=args.webui_root,
        runtime_dir=runtime_dir,
    )
    _print_preflight(preflight.as_dict(), output_format=args.format)
    return 0 if preflight.allowed else 1


def _default_webui_root() -> Path:
    return Path(__file__).resolve().parents[3] / "webui"


def _run_daemon(*, args: argparse.Namespace, runtime_dir: Path) -> int:
    update_module_installer_daemon_status(
        runtime_dir=runtime_dir,
        patch={
            "status": "running",
            "mode": "once" if args.daemon_once else "daemon",
            "started_at": _now(),
            "current_request_id": None,
        },
    )
    try:
        while True:
            update_module_installer_daemon_status(
                runtime_dir=runtime_dir,
                patch={
                    "status": "polling",
                    "last_poll_at": _now(),
                    "current_request_id": None,
                },
            )
            request = claim_next_module_installer_request(runtime_dir=runtime_dir)
            if request is not None:
                request_id = str(request["request_id"])
                update_module_installer_daemon_status(
                    runtime_dir=runtime_dir,
                    patch={
                        "status": "processing",
                        "current_request_id": request_id,
                        "last_claimed_at": _now(),
                    },
                )
                _process_request(request=request, args=args, runtime_dir=runtime_dir)
                update_module_installer_daemon_status(
                    runtime_dir=runtime_dir,
                    patch={
                        "status": "polling",
                        "current_request_id": None,
                        "last_completed_request_id": request_id,
                    },
                )
                if args.daemon_once:
                    return 0
            elif args.daemon_once:
                return 0
            time.sleep(max(args.poll_interval_seconds, 0.2))
    finally:
        update_module_installer_daemon_status(
            runtime_dir=runtime_dir,
            patch={
                "status": "stopped",
                "stopped_at": _now(),
                "current_request_id": None,
            },
        )


def _process_request(*, request: dict[str, object], args: argparse.Namespace, runtime_dir: Path) -> None:
    request_id = str(request["request_id"])
    options = request.get("options") if isinstance(request.get("options"), dict) else {}
    configure_database(str(args.database_url))
    _emit_installer_daemon_notification(request, event_kind="module_installer.request.running")
    try:
        available = available_module_manifests(ignore_load_errors=True)
        with get_database().session() as session:
            configured = configured_enabled_modules(settings.enabled_modules)
            desired = saved_desired_enabled_modules(session, configured)
            plan = saved_module_install_plan(session)
            result = supervise_module_install_plan(
                session=session,
                plan=plan,
                available=available,
                current_enabled=desired,
                desired_enabled=desired,
                database_url=str(args.database_url),
                runtime_dir=runtime_dir,
                webui_root=_path_option(options, "webui_root") or args.webui_root,
                npm_bin=_str_option(options, "npm_bin") or args.npm_bin,
                build_webui=_bool_option(options, "build_webui", args.build_webui),
                migrate_database=_bool_option(options, "migrate_database", args.migrate),
                database_backup_command=_str_option(options, "database_backup_command") or args.database_backup_command,
                database_restore_command=_str_option(options, "database_restore_command") or args.database_restore_command,
                database_restore_check_command=_str_option(options, "database_restore_check_command") or args.database_restore_check_command,
                activate_installed_modules=_bool_option(options, "activate_installed_modules", not args.no_activate_installed_modules),
                remove_uninstalled_modules_from_desired=_bool_option(options, "remove_uninstalled_modules_from_desired", not args.keep_uninstalled_modules_in_desired),
                restart_commands=_list_option(options, "restart_commands") or _list_option(options, "restart_command") or ([args.restart_command] if args.restart_command else []),
                health_urls=_list_option(options, "health_urls") or _list_option(options, "health_url") or ([args.health_url] if args.health_url else []),
                health_timeout_seconds=_float_option(options, "health_timeout_seconds", args.health_timeout_seconds),
                health_interval_seconds=_float_option(options, "health_interval_seconds", args.health_interval_seconds),
                request_context=_request_context(request),
            )
        updated_request = update_module_installer_request(
            runtime_dir=runtime_dir,
            request_id=request_id,
            patch={
                "status": "completed" if result.return_code == 0 else "failed",
                "finished_at": _now(),
                "result": result.as_dict(),
            },
        )
        _emit_installer_daemon_notification(
            updated_request,
            event_kind="module_installer.request.completed" if result.return_code == 0 else "module_installer.request.failed",
        )
    except Exception as exc:
        updated_request = update_module_installer_request(
            runtime_dir=runtime_dir,
            request_id=request_id,
            patch={
                "status": "failed",
                "finished_at": _now(),
                "error": str(exc),
            },
        )
        _emit_installer_daemon_notification(updated_request, event_kind="module_installer.request.failed")


def _emit_installer_daemon_notification(request: dict[str, object], *, event_kind: str) -> None:
    tenant_id = str(request.get("tenant_id") or "")
    if not tenant_id:
        return
    registry = build_runtime_notification_registry(settings)
    if registry is None:
        return
    status_value = str(request.get("status") or event_kind.rsplit(".", 1)[-1])
    try:
        with get_database().SessionLocal() as session:
            emitted = emit_module_installer_notification(
                session=session,
                registry=registry,
                tenant_id=tenant_id,
                request=request,
                event_kind=event_kind,
                subject=installer_notification_subject(event_kind, request),
                body_text=installer_notification_body(event_kind, request),
                recipient_id=str(request.get("requested_by") or "") or None,
                priority=installer_notification_priority(status_value),
            )
            if emitted:
                session.commit()
    except Exception:  # noqa: BLE001 - notification bridge must not block installer work.
        return


def _request_options_from_args(args: argparse.Namespace) -> dict[str, object]:
    return {
        "webui_root": str(args.webui_root) if args.webui_root else None,
        "npm_bin": args.npm_bin,
        "build_webui": args.build_webui,
        "migrate_database": args.migrate,
        "database_backup_command": args.database_backup_command,
        "database_restore_command": args.database_restore_command,
        "database_restore_check_command": args.database_restore_check_command,
        "activate_installed_modules": not args.no_activate_installed_modules,
        "remove_uninstalled_modules_from_desired": not args.keep_uninstalled_modules_in_desired,
        "restart_commands": [args.restart_command] if args.restart_command else [],
        "health_urls": [args.health_url] if args.health_url else [],
        "health_timeout_seconds": args.health_timeout_seconds,
        "health_interval_seconds": args.health_interval_seconds,
    }


def _request_context(request: dict[str, object]) -> dict[str, object]:
    context: dict[str, object] = {
        "request_id": request.get("request_id"),
        "requested_by": request.get("requested_by"),
        "tenant_id": request.get("tenant_id"),
    }
    if request.get("retry_of"):
        context["retry_of"] = request["retry_of"]
    if isinstance(request.get("trace"), dict):
        context["trace"] = request["trace"]
    return {key: value for key, value in context.items() if value is not None}


def _str_option(options: object, key: str) -> str | None:
    if not isinstance(options, dict):
        return None
    value = options.get(key)
    if isinstance(value, str) and value.strip():
        return value.strip()
    return None


def _path_option(options: object, key: str) -> Path | None:
    value = _str_option(options, key)
    return Path(value).expanduser() if value else None


def _bool_option(options: object, key: str, default: bool) -> bool:
    if not isinstance(options, dict):
        return default
    value = options.get(key)
    return value if isinstance(value, bool) else default


def _float_option(options: object, key: str, default: float) -> float:
    if not isinstance(options, dict):
        return default
    value = options.get(key)
    if isinstance(value, int | float):
        return float(value)
    return default


def _list_option(options: object, key: str) -> list[str]:
    if not isinstance(options, dict):
        return []
    value = options.get(key)
    if isinstance(value, str):
        return [item.strip() for item in value.splitlines() if item.strip()]
    if isinstance(value, list):
        return [str(item).strip() for item in value if str(item).strip()]
    return []


def _trusted_keys_from_args(values: list[str], *, flag_name: str) -> dict[str, str] | None:
    if not values:
        return None
    keys: dict[str, str] = {}
    for value in values:
        key_id, separator, public_key = value.partition("=")
        if not separator or not key_id.strip() or not public_key.strip():
            raise ModuleInstallerError(f"{flag_name} must use KEY_ID=BASE64_PUBLIC_KEY.")
        keys[key_id.strip()] = public_key.strip()
    return keys


def _now() -> str:
    from datetime import UTC, datetime

    return datetime.now(tz=UTC).isoformat()


def _print_preflight(payload: dict[str, object], *, output_format: str) -> None:
    if output_format == "json":
        print(json.dumps(payload, indent=2, sort_keys=True))
        return
    print(f"# Allowed: {payload['allowed']}")
    print(f"# Maintenance mode: {payload['maintenance_mode']}")
    print(f"# Frontend rebuild required: {payload['frontend_rebuild_required']}")
    for issue in payload.get("issues", []):
        if not isinstance(issue, dict):
            continue
        print(f"# {issue.get('severity')}: {issue.get('code')}: {issue.get('message')}")
    commands = payload.get("commands")
    if isinstance(commands, list) and commands:
        print("")
        for command in commands:
            print(command)
    checklist = payload.get("checklist")
    if isinstance(checklist, list) and checklist:
        print("")
        for item in checklist:
            if not isinstance(item, dict):
                continue
            detail = f": {item.get('detail')}" if item.get("detail") else ""
            print(f"# checklist {item.get('status')}: {item.get('label')}{detail}")


def _print_result(payload: dict[str, object], *, output_format: str) -> None:
    if output_format == "json":
        print(json.dumps(payload, indent=2, sort_keys=True))
        return
    if "runs" in payload or "requests" in payload or "locked" in payload or "request_id" in payload or "run_id" not in payload:
        print(json.dumps(payload, indent=2, sort_keys=True))
        return
    print(f"# Run: {payload['run_id']}")
    print(f"# Status: {payload['status']}")
    print(f"# Record: {payload['record_path']}")
    if payload.get("error"):
        print(f"# Error: {payload['error']}")
    commands = payload.get("commands")
    if isinstance(commands, list) and commands:
        print("")
        for command in commands:
            print(command)


if __name__ == "__main__":
    raise SystemExit(main())
