import os
from dotenv import load_dotenv
load_dotenv()

import time
import logging
import asyncio
import base64
import hmac
import importlib
import ipaddress
import secrets
import hashlib as _hashlib_mod
from dataclasses import dataclass, field
from typing import Any
from json import JSONDecodeError

APP_VERSION = "0.9.84"

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
_start_time = time.time()
_MESH_ONLY = os.environ.get("MESH_ONLY", "").strip().lower() in ("1", "true", "yes")
_HEADLESS_MESH_NODE_RUNTIME = os.environ.get("SHADOWBROKER_MESH_NODE_RUNTIME", "").strip().lower() in (
    "1",
    "true",
    "yes",
)
_WARNED_LEGACY_DM_PUBKEY_LOOKUPS: set[str] = set()


def _warn_legacy_dm_pubkey_lookup(agent_id: str) -> None:
    peer_id = str(agent_id or "").strip().lower()
    if not peer_id or peer_id in _WARNED_LEGACY_DM_PUBKEY_LOOKUPS:
        return
    _WARNED_LEGACY_DM_PUBKEY_LOOKUPS.add(peer_id)
    logger.warning(
        "mesh legacy DH pubkey lookup used for %s via direct agent_id; prefer invite-scoped lookup handles before removal in %s",
        stable_metadata_log_ref(peer_id, prefix="peer"),
        sunset_target_label(LEGACY_AGENT_ID_LOOKUP_TARGET),
    )


def _preferred_dm_lookup_target(agent_id: str = "", lookup_token: str = "") -> tuple[str, str]:
    resolved_id = str(agent_id or "").strip()
    resolved_lookup = str(lookup_token or "").strip()
    if resolved_lookup or not resolved_id:
        return resolved_id, resolved_lookup
    try:
        from services.mesh.mesh_wormhole_contacts import preferred_prekey_lookup_handle

        resolved_lookup = preferred_prekey_lookup_handle(resolved_id)
    except Exception:
        resolved_lookup = ""
    return resolved_id, resolved_lookup


def _compatibility_debt_status(snapshot: dict[str, Any] | None) -> dict[str, Any]:
    current = dict(snapshot or {})
    usage = dict(current.get("usage") or {})
    sunset = dict(current.get("sunset") or {})
    legacy_lookup = dict(usage.get("legacy_agent_id_lookup") or {})
    legacy_dm_get = dict(usage.get("legacy_dm_get") or {})
    dm_get_sunset = dict(sunset.get("legacy_dm_get") or {})
    return {
        "legacy_lookup_reliance": {
            "active": int(legacy_lookup.get("count", 0) or 0) > 0,
            "last_seen_at": int(legacy_lookup.get("last_seen_at", 0) or 0),
            "blocked_count": int(legacy_lookup.get("blocked_count", 0) or 0),
        },
        "legacy_mailbox_get_reliance": {
            "active": int(legacy_dm_get.get("count", 0) or 0) > 0,
            "last_seen_at": int(legacy_dm_get.get("last_seen_at", 0) or 0),
            "blocked_count": int(legacy_dm_get.get("blocked_count", 0) or 0),
            "enabled": not bool(dm_get_sunset.get("blocked", True)),
        },
    }


def _compatibility_readiness_status(snapshot: dict[str, Any] | None) -> dict[str, Any]:
    current = dict(snapshot or {})
    compatibility_debt = _compatibility_debt_status(current)
    try:
        from services.mesh.mesh_wormhole_contacts import compatibility_lookup_readiness_snapshot

        contact_readiness = dict(compatibility_lookup_readiness_snapshot() or {})
    except Exception:
        contact_readiness = {}
    legacy_lookup_runtime = dict(compatibility_debt.get("legacy_lookup_reliance") or {})
    legacy_mailbox_runtime = dict(compatibility_debt.get("legacy_mailbox_get_reliance") or {})
    return {
        "stored_legacy_lookup_contacts_present": bool(
            contact_readiness.get("stored_legacy_lookup_contacts_present", False)
        ),
        "stored_legacy_lookup_contacts": int(
            contact_readiness.get("stored_legacy_lookup_contacts", 0) or 0
        ),
        "stored_invite_lookup_contacts": int(
            contact_readiness.get("stored_invite_lookup_contacts", 0) or 0
        ),
        "legacy_lookup_runtime_active": bool(legacy_lookup_runtime.get("active", False)),
        "legacy_mailbox_get_runtime_active": bool(legacy_mailbox_runtime.get("active", False)),
        "legacy_mailbox_get_enabled": bool(legacy_mailbox_runtime.get("enabled", False)),
    }


def _scope_allows_exact_local(required_scopes: set[str], allowed_scopes: list[str]) -> bool:
    normalized_required = {str(scope or "").strip() for scope in required_scopes if str(scope or "").strip()}
    normalized_allowed = {
        str(scope or "").strip()
        for scope in list(allowed_scopes or [])
        if str(scope or "").strip()
    }
    return bool(normalized_allowed & normalized_required or "*" in normalized_allowed)


def gate_privileged_access_status_snapshot() -> dict[str, Any]:
    return _gate_privileged_access_status_snapshot_local()


def _check_explicit_scoped_auth_local(
    request: "Request",
    required_scopes: set[str],
) -> tuple[bool, str, str]:
    admin_key = _current_admin_key()
    scoped_tokens = _scoped_admin_tokens()
    presented = str(request.headers.get("X-Admin-Key", "") or "").strip()
    client = getattr(request, "client", None)
    host = (getattr(client, "host", "") or "").lower() if client else ""
    if admin_key and hmac.compare_digest(presented.encode(), admin_key.encode()):
        return True, "ok", "admin_key"
    if presented:
        presented_bytes = presented.encode()
        for token_value, scopes in scoped_tokens.items():
            if hmac.compare_digest(presented_bytes, str(token_value or "").encode()):
                if _scope_allows_exact_local(required_scopes, scopes):
                    return True, "ok", "explicit_scoped_token"
                return False, "insufficient scope", ""
    if not admin_key and not scoped_tokens:
        if _allow_insecure_admin() or (_debug_mode_enabled() and host == "test"):
            return True, "ok", "debug_override"
        return False, "Forbidden ΓÇö admin key not configured", ""
    return False, "Forbidden ΓÇö invalid or missing admin key", ""


def _gate_privileged_access_status_snapshot_local() -> dict[str, Any]:
    scoped_tokens = _scoped_admin_tokens()
    explicit_audit_configured = any(
        _scope_allows_exact_local({"gate.audit", "mesh.audit"}, scopes)
        for scopes in scoped_tokens.values()
    )
    admin_enabled = bool(_current_admin_key()) or bool(_allow_insecure_admin()) or bool(
        _debug_mode_enabled()
    )
    return {
        "ordinary_gate_view_scope_class": "gate_member_or_gate_scope",
        "privileged_gate_event_scope_class": "explicit_gate_audit",
        "repair_detail_scope_class": "local_operator_diagnostic",
        "privileged_gate_event_view_enabled": bool(admin_enabled or explicit_audit_configured),
        "repair_detail_view_enabled": True,
    }

# ---------------------------------------------------------------------------
# Docker Swarm Secrets support
# For each VAR below, if VAR_FILE is set (e.g. AIS_API_KEY_FILE=/run/secrets/AIS_API_KEY),
# the file is read and its trimmed content is placed into VAR.
# This MUST run before service imports ├óΓé¼ΓÇ¥ modules read os.environ at import time.
# ---------------------------------------------------------------------------
_SECRET_VARS = [
    "AIS_API_KEY",
    "AISHUB_USERNAME",
    "OPENSKY_CLIENT_ID",
    "OPENSKY_CLIENT_SECRET",
    "LTA_ACCOUNT_KEY",
    "CORS_ORIGINS",
    "ADMIN_KEY",
    "SHODAN_API_KEY",
    "FINNHUB_API_KEY",
    "MESH_SECURE_STORAGE_SECRET",
]

for _var in _SECRET_VARS:
    _file_var = f"{_var}_FILE"
    _file_path = os.environ.get(_file_var)
    if _file_path:
        try:
            with open(_file_path, "r") as _f:
                _value = _f.read().strip()
            if _value:
                os.environ[_var] = _value
                logger.info(f"Loaded secret {_var} from {_file_path}")
            else:
                logger.warning(f"Secret file {_file_path} for {_var} is empty")
        except FileNotFoundError:
            logger.error(f"Secret file {_file_path} for {_var} not found")
        except Exception as _e:
            logger.error(f"Failed to read secret file {_file_path} for {_var}: {_e}")

from fastapi import APIRouter, FastAPI, Request, Response, Query, Depends, HTTPException
from fastapi.exception_handlers import http_exception_handler as fastapi_http_exception_handler
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.background import BackgroundTask
from contextlib import asynccontextmanager
if not _MESH_ONLY:
    from services.data_fetcher import (
        start_scheduler,
        stop_scheduler,
        get_latest_data,
        seed_startup_caches,
    )
    from services.ais_stream import start_ais_stream, stop_ais_stream
    from services.carrier_tracker import start_carrier_tracker, stop_carrier_tracker
else:
    # Lean mesh/wormhole process ΓÇö avoid importing the OSINT fetcher graph.
    def start_scheduler(*_a, **_k):  # type: ignore[misc]
        return None

    def stop_scheduler(*_a, **_k):  # type: ignore[misc]
        return None

    def get_latest_data():  # type: ignore[misc]
        return {}

    def seed_startup_caches(*_a, **_k):  # type: ignore[misc]
        return None

    def start_ais_stream(*_a, **_k):  # type: ignore[misc]
        return None

    def stop_ais_stream(*_a, **_k):  # type: ignore[misc]
        return None

    def start_carrier_tracker(*_a, **_k):  # type: ignore[misc]
        return None

    def stop_carrier_tracker(*_a, **_k):  # type: ignore[misc]
        return None
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from services.schemas import HealthResponse, RefreshResponse
from services.config import get_settings
import uvicorn
import hashlib
import math
import json as json_mod
import orjson
import socket
from cachetools import TTLCache
import threading
from services.mesh.mesh_crypto import (
    _derive_peer_key,
    derive_node_id,
    normalize_peer_url,
    resolve_peer_key_for_url,
    verify_node_binding,
    parse_public_key_algo,
)
from services.mesh.mesh_compatibility import (
    LEGACY_AGENT_ID_LOOKUP_TARGET,
    compatibility_status_snapshot,
    legacy_agent_id_lookup_blocked,
    legacy_dm1_override_active,
    legacy_dm_get_override_active,
    record_legacy_agent_id_lookup,
    record_legacy_dm_get,
    sunset_target_label,
)
from services.mesh.mesh_protocol import (
    PROTOCOL_VERSION,
    normalize_payload,
)
from services.mesh.mesh_hashchain import GENESIS_HASH
from services.mesh.mesh_signed_events import (
    MeshWriteExemption,
    SignedWriteKind,
    get_prepared_signed_write,
    mesh_write_exempt,
    recover_verified_gate_reply_to as _shared_recover_verified_gate_reply_to,
    requires_signed_write,
    verify_gate_message_signed_write as _shared_verify_gate_message_signed_write,
    verify_signed_write as _shared_verify_signed_write,
    preflight_signed_event_integrity as _shared_preflight_signed_event_integrity,
    verify_key_rotation_claim_signature,
    verify_node_bound_signature,
    verify_signed_event as _shared_verify_signed_event,
)
from services.mesh.mesh_schema import validate_event_payload
from services.mesh.mesh_privacy_policy import (
    canonical_release_state,
    evaluate_network_release,
    network_release_state,
    queued_delivery_status,
    release_lane_required_tier,
)
from services.mesh.mesh_local_custody import local_custody_status_snapshot
from services.mesh.mesh_metadata_exposure import (
    dm_mailbox_response_view,
    dm_lookup_response_view,
    metadata_exposure_for_request,
    stable_metadata_log_ref,
)
from services.mesh.mesh_private_outbox import private_delivery_outbox
from services.mesh.mesh_private_release_worker import private_release_worker
from services.mesh.mesh_private_transport_manager import private_transport_manager
from services.mesh.mesh_privacy_prewarm import privacy_prewarm_service
from services.mesh.mesh_infonet_sync_support import (
    SyncWorkerState,
    begin_sync,
    eligible_sync_peers,
    finish_sync,
    finish_solo_sync,
    should_run_sync,
)
from services.mesh.mesh_router import (
    authenticated_push_peer_urls,
    configured_relay_peer_urls,
    parse_configured_relay_peers,
    peer_transport_kind,
)

from limiter import limiter
from auth import (
    _allow_insecure_admin,
    _anonymous_mode_state,
    _check_scoped_auth,
    _current_admin_key,
    _current_private_lane_tier,
    _debug_mode_enabled,
    _is_anonymous_dm_action_path,
    _is_anonymous_mesh_write_path,
    _is_anonymous_wormhole_gate_admin_path,
    _is_debug_test_request,
    _is_private_plane_access_path,
    _is_sensitive_no_store_path,
    _minimum_transport_tier,
    _private_plane_access_denied_payload,
    _private_infonet_policy_snapshot,
    _private_plane_refusal_response,
    _request_scope_path,
    _scoped_admin_tokens,
    _scoped_view_authenticated as _scoped_view_authenticated_auth,
    _security_headers,
    _strong_claims_policy_snapshot,
    _transport_tier_precondition_payload,
    _transport_tier_is_sufficient,
    _transport_tier_precondition,
    require_admin,
    require_local_operator,
    _validate_admin_startup,
    _validate_insecure_admin_startup,
    _validate_peer_push_secret,
    _verify_peer_push_hmac,
    _verify_peer_transport_hmac,
)
from node_state import (
    _NODE_BOOTSTRAP_STATE,
    _NODE_PUSH_STATE,
    _NODE_RUNTIME_LOCK,
    _NODE_SYNC_STOP,
    get_sync_state,
    set_sync_state,
)

# ---------------------------------------------------------------------------
# Router imports
# ---------------------------------------------------------------------------
def _load_optional_router(module_name: str) -> APIRouter:
    try:
        module = importlib.import_module(module_name)
        router = getattr(module, "router", None)
        if isinstance(router, APIRouter):
            return router
        logger.warning("Router module %s did not expose an APIRouter", module_name)
    except Exception as exc:
        logger.warning("Skipping router %s during startup: %s", module_name, type(exc).__name__)
    return APIRouter()


health_router = _load_optional_router("routers.health")
mesh_peer_sync_router = _load_optional_router("routers.mesh_peer_sync")
mesh_operator_router = _load_optional_router("routers.mesh_operator")
mesh_oracle_router = _load_optional_router("routers.mesh_oracle")
mesh_dm_router = _load_optional_router("routers.mesh_dm")
mesh_public_router = _load_optional_router("routers.mesh_public")
wormhole_router = _load_optional_router("routers.wormhole")
infonet_router = _load_optional_router("routers.infonet")

if _MESH_ONLY:
    # Empty routers keep include_router() call sites unchanged without loading OSINT.
    cctv_router = APIRouter()
    radio_router = APIRouter()
    sigint_router = APIRouter()
    tools_router = APIRouter()
    admin_router = APIRouter()
    data_router = APIRouter()
    ai_intel_router = APIRouter()
    sar_router = APIRouter()
    road_corridors_router = APIRouter()
    osint_router = APIRouter()
    scm_router = APIRouter()
    entity_graph_router = APIRouter()
    intel_feeds_router = APIRouter()
    analytics_router = APIRouter()
    agent_shell_router = APIRouter()
else:
    cctv_router = _load_optional_router("routers.cctv")
    radio_router = _load_optional_router("routers.radio")
    sigint_router = _load_optional_router("routers.sigint")
    tools_router = _load_optional_router("routers.tools")
    admin_router = _load_optional_router("routers.admin")
    data_router = _load_optional_router("routers.data")
    ai_intel_router = _load_optional_router("routers.ai_intel")
    sar_router = _load_optional_router("routers.sar")
    road_corridors_router = _load_optional_router("routers.road_corridors")
    osint_router = _load_optional_router("routers.osint")
    scm_router = _load_optional_router("routers.scm")
    entity_graph_router = _load_optional_router("routers.entity_graph")
    intel_feeds_router = _load_optional_router("routers.intel_feeds")
    analytics_router = _load_optional_router("routers.analytics")
    agent_shell_router = _load_optional_router("routers.agent_shell")


# ---------------------------------------------------------------------------
# Local overrides: keep these in main.py so tests that monkeypatch
# main._check_scoped_auth also affect _scoped_view_authenticated.
# ---------------------------------------------------------------------------
def _scoped_view_authenticated(request, scope: str) -> bool:  # type: ignore[override]
    ok, _detail = _check_scoped_auth(request, scope)
    if ok:
        return True
    return _is_debug_test_request(request)


def _privacy_core_status() -> dict[str, Any]:
    try:
        from services.privacy_core_attestation import privacy_core_attestation

        return dict(privacy_core_attestation())
    except Exception as exc:
        return {
            "available": False,
            "version": "",
            "loaded_version": "",
            "library_path": "",
            "loaded_hash": "",
            "library_sha256": "",
            "attestation_state": "attestation_stale_or_unknown",
            "trusted_hash": "",
            "manifest_source": "",
            "override_active": False,
            "detail": str(exc) or type(exc).__name__,
        }


def _privacy_claims_status(
    *,
    current_tier: str,
    local_custody: dict[str, Any] | None = None,
    privacy_core: dict[str, Any] | None = None,
    compatibility_readiness: dict[str, Any] | None = None,
    gate_privilege_access: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import privacy_claims_snapshot

    return privacy_claims_snapshot(
        transport_tier=current_tier,
        local_custody=dict(local_custody or {}),
        privacy_core=dict(privacy_core or {}),
        compatibility_readiness=dict(compatibility_readiness or {}),
        gate_privilege_access=dict(gate_privilege_access or {}),
    )


def _privacy_status_surface(
    *,
    privacy_claims: dict[str, Any] | None = None,
    strong_claims_allowed: bool | None = None,
    release_gate_ready: bool | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import privacy_status_surface_chip

    return privacy_status_surface_chip(
        dict(privacy_claims or {}),
        strong_claims_allowed=strong_claims_allowed,
        release_gate_ready=release_gate_ready,
    )


def _rollout_readiness_status(
    *,
    privacy_claims: dict[str, Any] | None = None,
    current_tier: str,
    local_custody: dict[str, Any] | None = None,
    privacy_core: dict[str, Any] | None = None,
    compatibility_debt: dict[str, Any] | None = None,
    compatibility_readiness: dict[str, Any] | None = None,
    gate_privilege_access: dict[str, Any] | None = None,
    strong_claims: dict[str, Any] | None = None,
    release_gate: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import rollout_readiness_snapshot

    return rollout_readiness_snapshot(
        privacy_claims=dict(privacy_claims or {}),
        transport_tier=current_tier,
        local_custody=dict(local_custody or {}),
        privacy_core=dict(privacy_core or {}),
        compatibility_debt=dict(compatibility_debt or {}),
        compatibility_readiness=dict(compatibility_readiness or {}),
        gate_privilege_access=dict(gate_privilege_access or {}),
        strong_claims=dict(strong_claims or {}),
        release_gate=dict(release_gate or {}),
    )


def _rollout_controls_status(
    *,
    rollout_readiness: dict[str, Any] | None = None,
    privacy_core: dict[str, Any] | None = None,
    strong_claims: dict[str, Any] | None = None,
    current_tier: str,
) -> dict[str, Any]:
    from services.privacy_claims import rollout_controls_snapshot

    return rollout_controls_snapshot(
        rollout_readiness=dict(rollout_readiness or {}),
        privacy_core=dict(privacy_core or {}),
        strong_claims=dict(strong_claims or {}),
        transport_tier=current_tier,
    )


def _rollout_health_status(
    *,
    rollout_readiness: dict[str, Any] | None = None,
    compatibility_debt: dict[str, Any] | None = None,
    compatibility_readiness: dict[str, Any] | None = None,
    lookup_handle_rotation: dict[str, Any] | None = None,
    gate_repair: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import rollout_health_snapshot

    return rollout_health_snapshot(
        rollout_readiness=dict(rollout_readiness or {}),
        compatibility_debt=dict(compatibility_debt or {}),
        compatibility_readiness=dict(compatibility_readiness or {}),
        lookup_handle_rotation=dict(lookup_handle_rotation or {}),
        gate_repair=dict(gate_repair or {}),
    )


def _strong_claims_compat_shim(
    snapshot: dict[str, Any],
    *,
    privacy_claims: dict[str, Any] | None = None,
    privacy_status: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import strong_claims_compat_shim

    return strong_claims_compat_shim(
        dict(snapshot or {}),
        privacy_claims=dict(privacy_claims or {}),
        privacy_status=dict(privacy_status or {}),
    )


def _release_gate_compat_shim_status(
    snapshot: dict[str, Any],
    *,
    privacy_claims: dict[str, Any] | None = None,
    rollout_readiness: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import release_gate_compat_shim

    return release_gate_compat_shim(
        dict(snapshot or {}),
        privacy_claims=dict(privacy_claims or {}),
        rollout_readiness=dict(rollout_readiness or {}),
    )


def _claim_surface_sources_status() -> dict[str, Any]:
    from services.privacy_claims import claim_surface_catalog

    return claim_surface_catalog()


def _review_export_status(
    *,
    privacy_claims: dict[str, Any] | None = None,
    rollout_readiness: dict[str, Any] | None = None,
    rollout_controls: dict[str, Any] | None = None,
    rollout_health: dict[str, Any] | None = None,
    claim_surface_sources: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import review_export_snapshot

    return review_export_snapshot(
        privacy_claims=dict(privacy_claims or {}),
        rollout_readiness=dict(rollout_readiness or {}),
        rollout_controls=dict(rollout_controls or {}),
        rollout_health=dict(rollout_health or {}),
        claim_surface_sources=dict(claim_surface_sources or {}),
    )


def _final_review_bundle_status(
    *,
    review_export: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import final_review_bundle_snapshot

    return final_review_bundle_snapshot(
        review_export=dict(review_export or {}),
    )


def _staged_rollout_telemetry_status(
    *,
    final_review_bundle: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import staged_rollout_telemetry_snapshot

    return staged_rollout_telemetry_snapshot(
        final_review_bundle=dict(final_review_bundle or {}),
    )


def _release_claims_matrix_status(
    *,
    final_review_bundle: dict[str, Any] | None = None,
    staged_rollout_telemetry: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import release_claims_matrix_snapshot

    return release_claims_matrix_snapshot(
        final_review_bundle=dict(final_review_bundle or {}),
        staged_rollout_telemetry=dict(staged_rollout_telemetry or {}),
    )


def _release_checklist_status(
    *,
    release_claims_matrix: dict[str, Any] | None = None,
    staged_rollout_telemetry: dict[str, Any] | None = None,
    final_review_bundle: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import release_checklist_snapshot

    return release_checklist_snapshot(
        release_claims_matrix=dict(release_claims_matrix or {}),
        staged_rollout_telemetry=dict(staged_rollout_telemetry or {}),
        final_review_bundle=dict(final_review_bundle or {}),
    )


def _explicit_review_export_status(
    *,
    final_review_bundle: dict[str, Any] | None = None,
    staged_rollout_telemetry: dict[str, Any] | None = None,
    release_claims_matrix: dict[str, Any] | None = None,
    release_checklist: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import explicit_review_export_snapshot

    return explicit_review_export_snapshot(
        final_review_bundle=dict(final_review_bundle or {}),
        staged_rollout_telemetry=dict(staged_rollout_telemetry or {}),
        release_claims_matrix=dict(release_claims_matrix or {}),
        release_checklist=dict(release_checklist or {}),
    )


def _review_manifest_status(
    *,
    explicit_review_export: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import review_manifest_snapshot

    return review_manifest_snapshot(
        explicit_review_export=dict(explicit_review_export or {}),
    )


def _review_consistency_status(
    *,
    explicit_review_export: dict[str, Any] | None = None,
    review_manifest: dict[str, Any] | None = None,
) -> dict[str, Any]:
    from services.privacy_claims import review_consistency_snapshot

    return review_consistency_snapshot(
        explicit_review_export=dict(explicit_review_export or {}),
        review_manifest=dict(review_manifest or {}),
    )


def _privacy_claim_surface_snapshot(
    *,
    current_tier: str,
    local_custody: dict[str, Any] | None = None,
    privacy_core: dict[str, Any] | None = None,
    contact_preference_refresh: dict[str, Any] | None = None,
) -> dict[str, Any]:
    claim_inputs = _privacy_claim_inputs_snapshot(
        contact_preference_refresh=contact_preference_refresh,
    )
    claims = _privacy_claims_status(
        current_tier=current_tier,
        local_custody=local_custody,
        privacy_core=privacy_core,
        compatibility_readiness=claim_inputs.get("compatibility_readiness"),
        gate_privilege_access=claim_inputs.get("gate_privilege_access"),
    )
    return {
        **claim_inputs,
        "privacy_claims": claims,
    }


def _diagnostic_review_package_snapshot(
    *,
    current_tier: str,
    local_custody: dict[str, Any] | None = None,
    privacy_core: dict[str, Any] | None = None,
    contact_preference_refresh: dict[str, Any] | None = None,
    lookup_handle_rotation: dict[str, Any] | None = None,
) -> dict[str, Any]:
    claim_surface = _privacy_claim_surface_snapshot(
        current_tier=current_tier,
        local_custody=dict(local_custody or {}),
        privacy_core=dict(privacy_core or {}),
        contact_preference_refresh=dict(contact_preference_refresh or {}),
    )
    strong_claims_raw = _strong_claims_policy_snapshot(
        current_tier=current_tier
    )
    release_gate_raw = _release_gate_status(
        current_tier=current_tier,
        strong_claims=strong_claims_raw,
        privacy_core=dict(privacy_core or {}),
        privacy_claims=claim_surface.get("privacy_claims"),
    )
    rollout_readiness = _rollout_readiness_status(
        privacy_claims=claim_surface.get("privacy_claims"),
        current_tier=current_tier,
        local_custody=dict(local_custody or {}),
        privacy_core=dict(privacy_core or {}),
        compatibility_debt=claim_surface.get("compatibility_debt"),
        compatibility_readiness=claim_surface.get("compatibility_readiness"),
        gate_privilege_access=claim_surface.get("gate_privilege_access"),
        strong_claims=strong_claims_raw,
        release_gate=release_gate_raw,
    )
    release_gate_surface = _release_gate_compat_shim_status(
        release_gate_raw,
        privacy_claims=claim_surface.get("privacy_claims"),
        rollout_readiness=rollout_readiness,
    )
    privacy_status = _privacy_status_surface(
        privacy_claims=claim_surface.get("privacy_claims"),
        strong_claims_allowed=strong_claims_raw.get("allowed"),
        release_gate_ready=release_gate_surface.get("ready"),
    )
    strong_claims_surface = _strong_claims_compat_shim(
        strong_claims_raw,
        privacy_claims=claim_surface.get("privacy_claims"),
        privacy_status=privacy_status,
    )
    claim_surface_sources = _claim_surface_sources_status()
    rollout_controls = _rollout_controls_status(
        rollout_readiness=rollout_readiness,
        privacy_core=dict(privacy_core or {}),
        strong_claims=strong_claims_raw,
        current_tier=current_tier,
    )
    rollout_health = _rollout_health_status(
        rollout_readiness=rollout_readiness,
        compatibility_debt=claim_surface.get("compatibility_debt"),
        compatibility_readiness=claim_surface.get("compatibility_readiness"),
        lookup_handle_rotation=dict(lookup_handle_rotation or {}),
    )
    review_export = _review_export_status(
        privacy_claims=claim_surface.get("privacy_claims"),
        rollout_readiness=rollout_readiness,
        rollout_controls=rollout_controls,
        rollout_health=rollout_health,
        claim_surface_sources=claim_surface_sources,
    )
    final_review_bundle = _final_review_bundle_status(
        review_export=review_export,
    )
    staged_rollout_telemetry = _staged_rollout_telemetry_status(
        final_review_bundle=final_review_bundle,
    )
    release_claims_matrix = _release_claims_matrix_status(
        final_review_bundle=final_review_bundle,
        staged_rollout_telemetry=staged_rollout_telemetry,
    )
    release_checklist = _release_checklist_status(
        release_claims_matrix=release_claims_matrix,
        staged_rollout_telemetry=staged_rollout_telemetry,
        final_review_bundle=final_review_bundle,
    )
    explicit_review_export = _explicit_review_export_status(
        final_review_bundle=final_review_bundle,
        staged_rollout_telemetry=staged_rollout_telemetry,
        release_claims_matrix=release_claims_matrix,
        release_checklist=release_checklist,
    )
    return {
        "claim_surface": claim_surface,
        "privacy_status": privacy_status,
        "strong_claims": strong_claims_surface,
        "release_gate": release_gate_surface,
        "rollout_readiness": rollout_readiness,
        "rollout_controls": rollout_controls,
        "rollout_health": rollout_health,
        "claim_surface_sources": claim_surface_sources,
        "review_export": review_export,
        "final_review_bundle": final_review_bundle,
        "staged_rollout_telemetry": staged_rollout_telemetry,
        "release_claims_matrix": release_claims_matrix,
        "release_checklist": release_checklist,
        "explicit_review_export": explicit_review_export,
    }


def _privacy_claim_inputs_snapshot(
    *,
    contact_preference_refresh: dict[str, Any] | None = None,
) -> dict[str, dict[str, Any]]:
    contact_refresh = dict(contact_preference_refresh or {})
    compatibility_debt: dict[str, Any] = {}
    compatibility_readiness: dict[str, Any] = {}
    compatibility_snapshot: dict[str, Any] = {}
    gate_privilege_access: dict[str, Any] = {}
    try:
        gate_privilege_access = dict(_gate_privileged_access_status_snapshot_local() or {})
    except Exception:
        gate_privilege_access = {}
    try:
        compatibility_snapshot = dict(compatibility_status_snapshot() or {})
        compatibility_debt = _compatibility_debt_status(compatibility_snapshot)
        compatibility_readiness = {
            **_compatibility_readiness_status(compatibility_snapshot),
            "local_contact_upgrade_ok": bool(contact_refresh.get("ok", False)),
            "upgraded_contact_preferences": int(
                contact_refresh.get("upgraded_contacts", 0) or 0
            ),
        }
    except Exception:
        compatibility_snapshot = {}
        compatibility_debt = {}
        compatibility_readiness = {}
    return {
        "compatibility_snapshot": compatibility_snapshot,
        "compatibility_debt": compatibility_debt,
        "compatibility_readiness": compatibility_readiness,
        "gate_privilege_access": gate_privilege_access,
    }


def _release_attestation_snapshot() -> dict[str, Any]:
    settings = get_settings()
    explicit_raw = str(
        getattr(settings, "MESH_RELEASE_ATTESTATION_PATH", "") or ""
    ).strip()
    default_path = Path(__file__).resolve().parent / "data" / "release_attestation.json"
    candidate = Path(explicit_raw) if explicit_raw else default_path
    if not candidate.is_absolute():
        candidate = Path(__file__).resolve().parent / candidate
    source = "env"
    relay_suite_green = bool(
        getattr(settings, "MESH_RELEASE_DM_RELAY_SECURITY_SUITE_GREEN", False)
    )
    detail = (
        "operator attestation present for the DM relay security suite"
        if relay_suite_green
        else "operator attestation for the DM relay security suite is missing"
    )
    generated_at = ""
    commit = ""
    threat_model_reference = "docs/mesh/threat-model.md"
    suite_report = ""
    suite_name = "dm_relay_security"
    workflow = ""
    run_id = ""
    run_attempt = ""
    ref = ""
    file_required = bool(explicit_raw)
    if candidate.exists():
        try:
            payload = orjson.loads(candidate.read_bytes())
            if not isinstance(payload, dict):
                raise ValueError("release attestation payload must be an object")
            source = "file"
            generated_at = str(payload.get("generated_at", "") or "").strip()
            commit = str(payload.get("commit", "") or "").strip()
            threat_model_reference = str(
                payload.get("threat_model_reference", threat_model_reference)
                or threat_model_reference
            ).strip()
            suite = dict(payload.get("dm_relay_security_suite") or {})
            ci = dict(payload.get("ci") or {})
            suite_name = str(suite.get("name", "") or "").strip() or suite_name
            suite_report = str(suite.get("report", "") or "").strip()
            workflow = str(ci.get("workflow", payload.get("workflow", "")) or "").strip()
            run_id = str(ci.get("run_id", payload.get("run_id", "")) or "").strip()
            run_attempt = str(
                ci.get("run_attempt", payload.get("run_attempt", "")) or ""
            ).strip()
            ref = str(ci.get("ref", payload.get("ref", "")) or "").strip()
            relay_suite_green = bool(
                suite.get(
                    "green",
                    payload.get(
                        "dm_relay_security_suite_green",
                        bool(
                            dict(payload.get("criteria") or {}).get(
                                "dm_relay_security_suite_green", False
                            )
                        ),
                    ),
                )
            )
            detail = str(
                suite.get(
                    "detail",
                    "release attestation confirms the DM relay security suite status",
                )
                or "release attestation confirms the DM relay security suite status"
            ).strip()
        except Exception as exc:
            source = "file_error"
            relay_suite_green = False
            detail = f"release attestation unreadable: {str(exc) or type(exc).__name__}"
    elif file_required:
        source = "file_missing"
        relay_suite_green = False
        detail = "configured release attestation file is missing"
    return {
        "source": source,
        "path": str(candidate),
        "generated_at": generated_at,
        "commit": commit,
        "dm_relay_security_suite_green": relay_suite_green,
        "detail": detail,
        "suite_name": suite_name,
        "suite_report": suite_report,
        "threat_model_reference": threat_model_reference,
        "workflow": workflow,
        "run_id": run_id,
        "run_attempt": run_attempt,
        "ref": ref,
    }


def _release_gate_status(
    *,
    current_tier: str | None = None,
    strong_claims: dict[str, Any] | None = None,
    privacy_core: dict[str, Any] | None = None,
    privacy_claims: dict[str, Any] | None = None,
) -> dict[str, Any]:
    snapshot = dict(
        strong_claims or _strong_claims_policy_snapshot(current_tier=current_tier)
    )
    privacy = dict(privacy_core or _privacy_core_status())
    authoritative_claims = dict((privacy_claims or {}).get("claims") or {})
    authoritative_dm = dict(authoritative_claims.get("dm_strong") or {})
    authoritative_gate = dict(authoritative_claims.get("gate_transitional") or {})
    compatibility = dict(snapshot.get("compatibility") or {})
    attestation = _release_attestation_snapshot()
    try:
        from services.release_profiles import profile_readiness_snapshot

        release_profile = profile_readiness_snapshot()
    except Exception:
        release_profile = {
            "profile": "dev",
            "allowed": False,
            "state": "release_profile_unknown",
            "blockers": ["release_profile_unavailable"],
            "detail": "release profile status unavailable",
        }
    relay_suite_green = bool(attestation.get("dm_relay_security_suite_green", False))
    privacy_core_attestation_state = str(
        privacy.get("attestation_state", "") or ""
    ).strip() or (
        "attested_current" if bool(privacy.get("policy_ok", False)) else "attestation_stale_or_unknown"
    )
    privacy_core_pinned = privacy_core_attestation_state == "attested_current"
    compat_overrides_off = bool(snapshot.get("compat_overrides_clear", False))
    clearnet_fallback_blocked = bool(snapshot.get("clearnet_fallback_blocked", False))
    gate_plaintext_persistence_off = not bool(
        compatibility.get("gate_plaintext_persist", False)
    )
    external_assurance_current = bool(snapshot.get("external_assurance_current", False))
    criteria: dict[str, dict[str, Any]] = {
        "dm_relay_security_suite_green": {
            "ok": relay_suite_green,
            "detail": str(attestation.get("detail", "") or "").strip()
            or (
                "release attestation confirms the DM relay security suite status"
                if relay_suite_green
                else "release attestation for the DM relay security suite is missing"
            ),
            "source": str(attestation.get("source", "env") or "env").strip(),
            "path": str(attestation.get("path", "") or "").strip(),
            "generated_at": str(attestation.get("generated_at", "") or "").strip(),
            "commit": str(attestation.get("commit", "") or "").strip(),
            "suite_name": str(attestation.get("suite_name", "") or "").strip(),
            "suite_report": str(attestation.get("suite_report", "") or "").strip(),
            "workflow": str(attestation.get("workflow", "") or "").strip(),
            "run_id": str(attestation.get("run_id", "") or "").strip(),
            "run_attempt": str(attestation.get("run_attempt", "") or "").strip(),
            "ref": str(attestation.get("ref", "") or "").strip(),
        },
        "privacy_core_pinned": {
            "ok": privacy_core_pinned,
            "detail": (
                "privacy-core artifact trust is current"
                if privacy_core_pinned
                else str(privacy.get("detail", "") or "").strip()
                or "privacy-core artifact trust is not currently attested"
            ),
            "attestation_state": privacy_core_attestation_state,
            "loaded_version": str(
                privacy.get("loaded_version", privacy.get("version", "")) or ""
            ).strip(),
            "loaded_hash": str(
                privacy.get("loaded_hash", privacy.get("library_sha256", "")) or ""
            ).strip(),
            "trusted_hash": str(privacy.get("trusted_hash", "") or "").strip(),
            "manifest_source": str(privacy.get("manifest_source", "") or "").strip(),
            "override_active": bool(privacy.get("override_active", False)),
        },
        "compat_overrides_off": {
            "ok": compat_overrides_off,
            "detail": (
                "compatibility sunset overrides are clear"
                if compat_overrides_off
                else "one or more compatibility sunset overrides are still active"
            ),
        },
        "clearnet_fallback_blocked": {
            "ok": clearnet_fallback_blocked,
            "detail": (
                "private-lane clearnet fallback is blocked"
                if clearnet_fallback_blocked
                else "private-lane clearnet fallback is still allowed"
            ),
        },
        "gate_plaintext_persistence_off": {
            "ok": gate_plaintext_persistence_off,
            "detail": (
                "durable gate plaintext persistence is off"
                if gate_plaintext_persistence_off
                else "durable gate plaintext persistence is enabled"
            ),
        },
        "external_assurance_current": {
            "ok": external_assurance_current,
            "detail": str(snapshot.get("external_assurance_detail", "") or "").strip()
            or (
                "external witness and transparency assurances are current"
                if external_assurance_current
                else "external assurance is not current"
            ),
            "state": str(
                snapshot.get("external_assurance_state", "unknown") or "unknown"
            ).strip(),
            "configured": bool(snapshot.get("external_assurance_configured", False)),
        },
        "release_profile_ready": {
            "ok": bool(release_profile.get("allowed", False)),
            "detail": str(release_profile.get("detail", "") or "").strip(),
            "profile": str(release_profile.get("profile", "") or "").strip(),
            "state": str(release_profile.get("state", "") or "").strip(),
            "blockers": list(release_profile.get("blockers") or []),
        },
    }
    if privacy_claims:
        criteria["authoritative_dm_claim_ready"] = {
            "ok": bool(authoritative_dm.get("allowed", False)),
            "detail": str(authoritative_dm.get("plain_label", "") or "").strip(),
            "state": str(authoritative_dm.get("state", "") or "").strip(),
        }
        criteria["authoritative_gate_claim_ready"] = {
            "ok": bool(authoritative_gate.get("allowed", False)),
            "detail": str(authoritative_gate.get("plain_label", "") or "").strip(),
            "state": str(authoritative_gate.get("state", "") or "").strip(),
        }
    blocking = [
        name
        for name, criterion in criteria.items()
        if not bool(criterion.get("ok", False))
    ]
    return {
        "ready": not blocking,
        "detail": "release gate satisfied" if not blocking else "release gate pending",
        "blocking_reasons": blocking,
        "next_action": blocking[0] if blocking else "",
        "criteria": criteria,
        "attestation": attestation,
        "release_profile": release_profile,
        "compatibility_shim": True,
        "source_model": "privacy_claims",
        "authoritative_dm_claim_state": str(authoritative_dm.get("state", "") or "").strip(),
        "authoritative_gate_claim_state": str(authoritative_gate.get("state", "") or "").strip(),
        "threat_model_reference": str(
            attestation.get("threat_model_reference", "docs/mesh/threat-model.md")
            or "docs/mesh/threat-model.md"
        ).strip(),
    }


def _validate_privacy_core_startup() -> None:
    # The wormhole child agent reuses this app on WORMHOLE_PORT; the parent
    # backend already validated privacy-core before spawning it.
    if os.environ.get("WORMHOLE_PORT"):
        return
    from services.privacy_core_attestation import validate_privacy_core_startup

    validate_privacy_core_startup()


def _public_mesh_log_entry(entry: dict[str, Any]) -> dict[str, Any] | None:
    tier_str = str((entry or {}).get("trust_tier", "public_degraded") or "public_degraded").strip().lower()
    if tier_str.startswith("private_"):
        return None
    return {
        "sender": str((entry or {}).get("sender", "") or ""),
        "destination": str((entry or {}).get("destination", "") or ""),
        "routed_via": str((entry or {}).get("routed_via", "") or ""),
        "priority": str((entry or {}).get("priority", "") or ""),
        "route_reason": str((entry or {}).get("route_reason", "") or ""),
        "timestamp": float((entry or {}).get("timestamp", 0) or 0),
    }


def _public_mesh_log_size(entries: list[dict[str, Any]]) -> int:
    return sum(1 for item in entries if _public_mesh_log_entry(item) is not None)


# Issue #243 (tg12): the public redaction now exposes only the bare
# "is Wormhole on?" boolean. Transport choice (tor/i2p/mixnet/direct),
# anonymous-mode state, and the named privacy profile are all
# operational posture and were leaking actionable recon to any
# unauthenticated caller. They are now gated behind authenticated reads
# (admin key or scoped-view token). Loopback Tauri shells and Docker
# bridge frontend containers continue to see full status because the
# Next.js catch-all proxy injects the configured ADMIN_KEY for
# same-origin/non-browser callers (see PR #263), so legitimate operator
# UX is unaffected.
_WORMHOLE_PUBLIC_SETTINGS_FIELDS = {"enabled"}
_WORMHOLE_PUBLIC_PROFILE_FIELDS = {"wormhole_enabled"}
_PRIVATE_LANE_CONTROL_FIELDS = {"private_lane_tier", "private_lane_policy"}
_PUBLIC_RNS_STATUS_FIELDS = {"enabled", "ready", "configured_peers", "active_peers"}
_NODE_PUBLIC_EVENT_HOOK_REGISTERED = False
_NODE_RUNTIME_THREADS_STARTED = False
_INFONET_PRIVATE_TRANSPORT_LOCK = threading.Lock()


def _current_node_mode() -> str:
    mode = str(get_settings().MESH_NODE_MODE or "participant").strip().lower()
    if mode not in {"participant", "relay", "perimeter"}:
        return "participant"
    return mode


def _node_runtime_supported() -> bool:
    return _current_node_mode() in {"participant", "relay"}


def _node_activation_enabled() -> bool:
    from services.node_settings import read_node_settings

    try:
        settings = read_node_settings()
    except Exception:
        return False
    return bool(settings.get("enabled", False))


def _participant_node_enabled() -> bool:
    return _node_runtime_supported() and _node_activation_enabled()


def _node_runtime_snapshot() -> dict[str, Any]:
    with _NODE_RUNTIME_LOCK:
        return {
            "node_mode": _current_node_mode(),
            "node_enabled": _participant_node_enabled(),
            "private_transport_required": _infonet_private_transport_required(),
            "bootstrap": {**dict(_NODE_BOOTSTRAP_STATE), "node_mode": _current_node_mode()},
            "sync_runtime": get_sync_state().to_dict(),
            "push_runtime": dict(_NODE_PUSH_STATE),
        }


def _set_node_sync_disabled_state(*, current_head: str = "") -> SyncWorkerState:
    return SyncWorkerState(
        current_head=str(current_head or ""),
        last_outcome="disabled",
    )


def _set_participant_node_enabled(enabled: bool) -> dict[str, Any]:
    from services.mesh.mesh_hashchain import infonet
    from services.node_settings import write_node_settings

    settings = write_node_settings(enabled=bool(enabled))
    current_head = str(infonet.head_hash or "")
    with _NODE_RUNTIME_LOCK:
        _NODE_BOOTSTRAP_STATE["node_mode"] = _current_node_mode()
        set_sync_state(
            SyncWorkerState(current_head=current_head)
            if bool(enabled) and _node_runtime_supported()
            else _set_node_sync_disabled_state(current_head=current_head)
        )
    return {
        **settings,
        "node_mode": _current_node_mode(),
        "node_enabled": _participant_node_enabled(),
    }


def _infonet_private_transport_required() -> bool:
    return not bool(getattr(get_settings(), "MESH_INFONET_ALLOW_CLEARNET_SYNC", False))


def _infonet_private_transport_error() -> str:
    return "private Infonet requires onion/RNS transport; no clearnet sync fallback"


def _is_private_infonet_transport(transport: str) -> bool:
    return str(transport or "").strip().lower() in {"onion", "rns"}


def _filter_infonet_sync_records(records: list[Any]) -> list[Any]:
    if not _infonet_private_transport_required():
        return records
    return [
        record
        for record in records
        if _is_private_infonet_transport(str(getattr(record, "transport", "") or ""))
    ]


def _infonet_peer_url_allowed(peer_url: str) -> bool:
    if not _infonet_private_transport_required():
        return True
    return _is_private_infonet_transport(peer_transport_kind(peer_url))


def _filter_infonet_peer_urls(peer_urls: list[str]) -> list[str]:
    if not _infonet_private_transport_required():
        return peer_urls
    return [peer_url for peer_url in peer_urls if _infonet_peer_url_allowed(peer_url)]


def _infonet_peer_requests_proxies(normalized_peer_url: str) -> dict[str, str] | None:
    """Return requests proxy settings for a sync/push peer, enforcing private policy."""
    transport = peer_transport_kind(normalized_peer_url)
    if _infonet_private_transport_required() and not _is_private_infonet_transport(transport):
        raise RuntimeError(_infonet_private_transport_error())
    if transport != "onion":
        return None
    if not bool(get_settings().MESH_ARTI_ENABLED):
        raise RuntimeError("onion peer requests require Arti to be enabled")
    from services.wormhole_supervisor import _check_arti_ready

    if not _check_arti_ready():
        raise RuntimeError("onion peer requests require a ready Arti transport")
    socks_port = int(get_settings().MESH_ARTI_SOCKS_PORT or 9050)
    proxy = f"socks5h://127.0.0.1:{socks_port}"
    return {"http": proxy, "https": proxy}


def _local_infonet_peer_url() -> str:
    """Return this node's advertised peer URL for HMAC peer authentication."""
    configured = normalize_peer_url(str(getattr(get_settings(), "MESH_PUBLIC_PEER_URL", "") or ""))
    if configured:
        return configured
    try:
        from services.tor_hidden_service import tor_service

        return normalize_peer_url(str(tor_service.onion_address or ""))
    except Exception:
        return ""


def _clear_stale_arti_sync_backoff() -> None:
    """Drop cached Arti warmup errors once SOCKS transport is actually ready."""
    from dataclasses import replace

    with _NODE_RUNTIME_LOCK:
        current = get_sync_state()
        error_lower = str(current.last_error or "").lower()
        if "arti" not in error_lower and "onion sync requires" not in error_lower:
            return
        set_sync_state(
            replace(
                current,
                last_error="",
                consecutive_failures=0,
                next_sync_due_at=int(time.time()),
                last_outcome="idle" if current.last_outcome == "error" else current.last_outcome,
            )
        )


def _ensure_infonet_private_transport_ready(reason: str = "") -> bool:
    """Warm the local onion transport before private Infonet sync.

    Infonet may know about an onion seed before the Wormhole UI is opened. The
    sync worker still needs Arti marked enabled and a ready SOCKS listener, so
    do that lazily in the worker instead of making users manually open another
    panel just to participate in the Infonet.
    """
    if not _infonet_private_transport_required():
        return True

    try:
        from services.wormhole_supervisor import _check_arti_ready

        if bool(get_settings().MESH_ARTI_ENABLED) and _check_arti_ready():
            return True
    except Exception:
        pass

    if not _INFONET_PRIVATE_TRANSPORT_LOCK.acquire(blocking=False):
        return False
    try:
        from routers.ai_intel import _write_env_value
        from services.tor_hidden_service import tor_service
        from services.wormhole_supervisor import _check_arti_ready

        label = f" ({reason})" if reason else ""
        logger.info("Infonet private transport warmup starting%s", label)
        from services.wormhole_supervisor import invalidate_arti_ready_cache

        for attempt in range(3):
            tor_result = tor_service.start(target_port=8000)
            if not tor_result.get("ok"):
                logger.warning(
                    "Infonet private transport warmup incomplete%s: %s",
                    label,
                    tor_result,
                )
                continue
            _write_env_value("MESH_ARTI_ENABLED", "true")
            get_settings.cache_clear()
            invalidate_arti_ready_cache()
            deadline = time.monotonic() + 30.0
            while time.monotonic() < deadline:
                if _check_arti_ready(force=True):
                    logger.info("Infonet private transport ready%s", label)
                    _clear_stale_arti_sync_backoff()
                    threading.Thread(target=_swarm_bootstrap_after_transport_ready, daemon=True).start()
                    _kick_public_sync_background(f"transport_ready{label}")
                    return True
                time.sleep(1.0)
            logger.warning(
                "Infonet private transport SOCKS not ready after Tor start (attempt %d/3)%s",
                attempt + 1,
                label,
            )
            tor_service.stop()
        logger.warning("Infonet private transport warmup incomplete%s", label)
        return False
    except Exception as exc:
        logger.warning("Infonet private transport warmup failed: %s", exc)
        return False
    finally:
        _INFONET_PRIVATE_TRANSPORT_LOCK.release()


def _configured_bootstrap_seed_peer_urls() -> list[str]:
    from services.mesh.mesh_fleet_defaults import configured_bootstrap_seed_peers_with_fleet_default

    settings = get_settings()
    primary = str(getattr(settings, "MESH_BOOTSTRAP_SEED_PEERS", "") or "").strip()
    legacy = str(getattr(settings, "MESH_DEFAULT_SYNC_PEERS", "") or "").strip()
    return configured_bootstrap_seed_peers_with_fleet_default(
        parse_configured_relay_peers(primary or legacy)
    )


def _refresh_node_peer_store(*, now: float | None = None) -> dict[str, Any]:
    from services.mesh.mesh_bootstrap_manifest import load_bootstrap_manifest_from_settings
    from services.mesh.mesh_peer_store import (
        DEFAULT_PEER_STORE_PATH,
        PeerStore,
        make_bootstrap_peer_record,
        make_push_peer_record,
        make_sync_peer_record,
    )

    timestamp = int(now if now is not None else time.time())
    mode = _current_node_mode()
    store = PeerStore(DEFAULT_PEER_STORE_PATH)
    try:
        store.load()
    except Exception:
        store = PeerStore(DEFAULT_PEER_STORE_PATH)

    private_transport_required = _infonet_private_transport_required()
    operator_peers = configured_relay_peer_urls()
    bootstrap_seed_peers = _configured_bootstrap_seed_peer_urls()
    skipped_clearnet_peers = 0
    pruned_clearnet_peers = 0
    if private_transport_required:
        for key, record in list(store._records.items()):
            if _is_private_infonet_transport(str(getattr(record, "transport", "") or "")):
                continue
            del store._records[key]
            pruned_clearnet_peers += 1
    for peer_url in operator_peers:
        transport = peer_transport_kind(peer_url)
        if not transport:
            continue
        if private_transport_required and not _is_private_infonet_transport(transport):
            skipped_clearnet_peers += 1
            continue
        store.upsert(
            make_sync_peer_record(
                peer_url=peer_url,
                transport=transport,
                role="relay",
                source="operator",
                now=timestamp,
            )
        )
        store.upsert(
            make_push_peer_record(
                peer_url=peer_url,
                transport=transport,
                role="relay",
                source="operator",
                now=timestamp,
            )
        )

    operator_peer_set = set(operator_peers)
    for peer_url in bootstrap_seed_peers:
        if peer_url in operator_peer_set:
            continue
        transport = peer_transport_kind(peer_url)
        if not transport:
            continue
        if private_transport_required and not _is_private_infonet_transport(transport):
            skipped_clearnet_peers += 1
            continue
        store.upsert(
            make_bootstrap_peer_record(
                peer_url=peer_url,
                transport=transport,
                role="seed",
                label="ShadowBroker bootstrap seed",
                signer_id="shadowbroker-bootstrap",
                now=timestamp,
            )
        )
        store.upsert(
            make_sync_peer_record(
                peer_url=peer_url,
                transport=transport,
                role="seed",
                source="bundle",
                label="ShadowBroker bootstrap seed",
                signer_id="shadowbroker-bootstrap",
                now=timestamp,
            )
        )

    manifest = None
    bootstrap_error = ""
    try:
        manifest = load_bootstrap_manifest_from_settings(now=timestamp)
    except Exception as exc:
        bootstrap_error = str(exc or "").strip()

    if manifest is not None:
        for peer in manifest.peers:
            if private_transport_required and not _is_private_infonet_transport(peer.transport):
                skipped_clearnet_peers += 1
                continue
            store.upsert(
                make_bootstrap_peer_record(
                    peer_url=peer.peer_url,
                    transport=peer.transport,
                    role=peer.role,
                    label=peer.label,
                    signer_id=manifest.signer_id,
                    now=timestamp,
                )
            )
            store.upsert(
                make_sync_peer_record(
                    peer_url=peer.peer_url,
                    transport=peer.transport,
                    role=peer.role,
                    source="bootstrap_promoted",
                    label=peer.label,
                    signer_id=manifest.signer_id,
                    now=timestamp,
                )
            )

    if private_transport_required and skipped_clearnet_peers and not bootstrap_error:
        bootstrap_error = _infonet_private_transport_error()

    swarm_pull: dict[str, Any] = {}
    try:
        from services.mesh.mesh_swarm_runtime import refresh_swarm_manifest_from_seeds

        swarm_pull = refresh_swarm_manifest_from_seeds(now=timestamp)
        if swarm_pull.get("ok") and not swarm_pull.get("skipped"):
            store.load()
    except Exception as exc:
        swarm_pull = {"ok": False, "detail": str(exc or type(exc).__name__)}

    store.save()
    bootstrap_records = store.records_for_bucket("bootstrap")
    sync_records = store.records_for_bucket("sync")
    push_records = store.records_for_bucket("push")
    if private_transport_required:
        bootstrap_records = [record for record in bootstrap_records if _is_private_infonet_transport(record.transport)]
        sync_records = [record for record in sync_records if _is_private_infonet_transport(record.transport)]
        push_records = [record for record in push_records if _is_private_infonet_transport(record.transport)]
    swarm_sync_peer_count = len([record for record in sync_records if str(record.source or "") == "swarm"])
    swarm_push_peer_count = len([record for record in push_records if str(record.source or "") == "swarm"])
    swarm_pull_ok = bool(swarm_pull.get("ok")) and not bool(swarm_pull.get("skipped"))
    if swarm_pull_ok or manifest is not None or swarm_sync_peer_count > 0:
        bootstrap_state = "ready"
        bootstrap_detail = "Bootstrap peer discovery is ready."
    elif bootstrap_seed_peers:
        bootstrap_state = "connecting"
        bootstrap_detail = "Bootstrap seeds are configured; local node stays active and retries in the background."
    else:
        bootstrap_state = "solo"
        bootstrap_detail = "No bootstrap seeds configured; local node is running in solo mode."
    snapshot = {
        "node_mode": mode,
        "private_transport_required": private_transport_required,
        "bootstrap_state": bootstrap_state,
        "bootstrap_detail": bootstrap_detail,
        "skipped_clearnet_peer_count": skipped_clearnet_peers,
        "pruned_clearnet_peer_count": pruned_clearnet_peers,
        "manifest_loaded": manifest is not None,
        "manifest_signer_id": manifest.signer_id if manifest is not None else "",
        "manifest_valid_until": int(manifest.valid_until or 0) if manifest is not None else 0,
        "bootstrap_peer_count": len(bootstrap_records),
        "sync_peer_count": len(sync_records),
        "push_peer_count": len(push_records),
        "swarm_sync_peer_count": swarm_sync_peer_count,
        "swarm_push_peer_count": swarm_push_peer_count,
        "operator_peer_count": len(operator_peers),
        "bootstrap_seed_peer_count": len(bootstrap_seed_peers),
        "default_sync_peer_count": len(bootstrap_seed_peers),
        "last_bootstrap_error": bootstrap_error,
        "swarm_manifest_pull": swarm_pull,
    }
    with _NODE_RUNTIME_LOCK:
        _NODE_BOOTSTRAP_STATE.update(snapshot)
    return snapshot


def _swarm_bootstrap_after_transport_ready() -> None:
    try:
        from services.mesh.mesh_swarm_runtime import join_swarm_with_retries

        join_swarm_with_retries(attempts=4, delay_s=15.0, force=True)
        _refresh_node_peer_store()
    except Exception:
        logger.warning("swarm bootstrap after transport ready failed", exc_info=True)


def _materialize_local_infonet_state() -> None:
    from services.mesh.mesh_hashchain import infonet

    infonet.ensure_materialized()
    try:
        _hydrate_gate_store_from_chain(list(infonet.events))
        _hydrate_dm_relay_from_chain(list(infonet.events))
    except Exception:
        pass


class PeerSyncHTTPError(RuntimeError):
    def __init__(self, status_code: int, detail: str, *, retry_after_s: int = 0):
        self.status_code = int(status_code or 0)
        self.retry_after_s = int(retry_after_s or 0)
        message = str(detail or f"HTTP {self.status_code}").strip()
        if not message.upper().startswith("HTTP"):
            message = f"HTTP {self.status_code}: {message}"
        super().__init__(message)


def _parse_retry_after_seconds(value: str) -> int:
    try:
        return max(0, int(float(str(value or "").strip())))
    except Exception:
        return 0


def _peer_sync_response(peer_url: str, body: dict[str, Any]) -> dict[str, Any]:
    import requests as _requests
    from services.wormhole_supervisor import _check_arti_ready

    normalized = normalize_peer_url(peer_url)
    if not normalized:
        raise ValueError("invalid peer URL")
    transport = peer_transport_kind(normalized)
    if _infonet_private_transport_required() and not _is_private_infonet_transport(transport):
        raise RuntimeError(_infonet_private_transport_error())

    settings = get_settings()
    timeout = int(
        getattr(settings, "MESH_SYNC_TIMEOUT_S", 0)
        or getattr(settings, "MESH_RELAY_PUSH_TIMEOUT_S", 0)
        or 10
    )
    kwargs: dict[str, Any] = {
        "json": body,
        "timeout": timeout,
        "headers": {"Content-Type": "application/json"},
    }
    if transport == "onion":
        if not bool(get_settings().MESH_ARTI_ENABLED):
            raise RuntimeError("onion sync requires Arti to be enabled")
        if not _check_arti_ready():
            raise RuntimeError("onion sync requires a ready Arti transport")
        socks_port = int(get_settings().MESH_ARTI_SOCKS_PORT or 9050)
        proxy = f"socks5h://127.0.0.1:{socks_port}"
        kwargs["proxies"] = {"http": proxy, "https": proxy}
    response = _requests.post(f"{normalized}/api/mesh/infonet/sync", **kwargs)
    # HTTP 429 must be surfaced as a typed exception carrying the
    # Retry-After value, so finish_sync can honor it and stop hammering
    # the upstream. Pre-fix this path just stringified the status into
    # a ValueError, which finish_sync then ignored ΓÇö keeping the
    # upstream's rate-limit bucket full indefinitely.
    if response.status_code == 429:
        from services.mesh.mesh_infonet_sync_support import (
            PeerSyncRateLimited,
            parse_retry_after_header,
        )

        retry_after_s = parse_retry_after_header(
            response.headers.get("Retry-After", "") or "",
        )
        try:
            body_text = response.text[:200]
        except Exception:
            body_text = ""
        raise PeerSyncRateLimited(
            f"HTTP 429 from {normalized} (retry_after={retry_after_s}s): {body_text}",
            retry_after_s=retry_after_s,
            status=429,
        )
    try:
        payload = response.json()
    except Exception as exc:
        raise ValueError(f"peer sync returned non-JSON response ({response.status_code})") from exc
    if response.status_code != 200:
        detail = str(payload.get("detail", "") or f"HTTP {response.status_code}").strip()
        retry_after_s = _parse_retry_after_seconds(response.headers.get("Retry-After", ""))
        raise PeerSyncHTTPError(response.status_code, detail, retry_after_s=retry_after_s)
    if not isinstance(payload, dict):
        raise ValueError("peer sync returned malformed payload")
    return payload


def _hydrate_gate_store_from_chain(events: list[dict]) -> int:
    """Copy any gate_message chain events into the local gate_store for read/decrypt.

    Only events that are resident in the local infonet (accepted or already
    present) are hydrated.  The canonical infonet-resident event is used ├óΓé¼ΓÇ¥
    never the raw batch event ├óΓé¼ΓÇ¥ so a forged batch entry carrying a valid
    event_id but attacker-chosen payload cannot pollute gate_store.
    """
    import copy

    from services.mesh.mesh_hashchain import gate_store, infonet

    count = 0
    for evt in events:
        if evt.get("event_type") != "gate_message":
            continue
        event_id = str(evt.get("event_id", "") or "").strip()
        if not event_id or event_id not in infonet.event_index:
            continue
        # Use the canonical infonet-resident event, not the raw batch event.
        canonical = infonet.events[infonet.event_index[event_id]]
        payload = canonical.get("payload") or {}
        gate_id = str(payload.get("gate", "") or "").strip()
        if not gate_id:
            continue
        try:
            gate_store.append(gate_id, copy.deepcopy(canonical))
            count += 1
        except Exception:
            pass
    return count


def _hydrate_dm_relay_from_chain(events: list[dict]) -> int:
    """Copy accepted dm_message chain events into the local encrypted DM relay."""
    import hashlib

    from services.mesh.mesh_dm_relay import dm_relay
    from services.mesh.mesh_hashchain import infonet

    count = 0
    for evt in events:
        if evt.get("event_type") != "dm_message":
            continue
        event_id = str(evt.get("event_id", "") or "").strip()
        if not event_id or event_id not in infonet.event_index:
            continue
        canonical = infonet.events[infonet.event_index[event_id]]
        payload = canonical.get("payload") if isinstance(canonical.get("payload"), dict) else {}
        sender_token_hash = hashlib.sha256(
            f"hashchain-dm-sender|{event_id}|{canonical.get('node_id', '')}".encode("utf-8")
        ).hexdigest()
        try:
            from services.mesh.mesh_dm_connect_delivery import relay_push_peer_urls_for_payload

            replication_urls = relay_push_peer_urls_for_payload(dict(payload))
        except Exception:
            replication_urls = []
        try:
            result = dm_relay.deposit(
                sender_id=str(canonical.get("node_id", "") or ""),
                raw_sender_id=str(canonical.get("node_id", "") or ""),
                recipient_id=str(payload.get("recipient_id", "") or ""),
                ciphertext=str(payload.get("ciphertext", "") or ""),
                msg_id=str(payload.get("msg_id", "") or ""),
                delivery_class=str(payload.get("delivery_class", "") or ""),
                recipient_token=str(payload.get("recipient_token", "") or "") or None,
                sender_seal=str(payload.get("sender_seal", "") or ""),
                sender_token_hash=sender_token_hash,
                payload_format=str(payload.get("format", "dm1") or "dm1"),
                session_welcome=str(payload.get("session_welcome", "") or ""),
                replication_peer_urls=replication_urls,
            )
            if result.get("ok"):
                count += 1
        except Exception:
            pass
    return count


def _sync_from_peer(
    peer_url: str,
    *,
    page_limit: int = 100,
    max_rounds: int = 5,
) -> tuple[bool, str, bool, int]:
    """Sync the local Infonet chain against ``peer_url``.

    Returns ``(ok, error, forked, retry_after_s)``. The fourth tuple
    element is non-zero only when the peer responded with HTTP 429
    and supplied a parseable ``Retry-After`` header ΓÇö see the typed
    ``PeerSyncRateLimited`` exception in mesh_infonet_sync_support.py.
    Callers should pass that value to ``finish_sync(retry_after_s=...)``
    so the next attempt actually waits.
    """
    from services.mesh.mesh_hashchain import infonet
    from services.mesh.mesh_infonet_sync_support import PeerSyncRateLimited

    rounds = 0
    while rounds < max_rounds:
        body = {
            "protocol_version": PROTOCOL_VERSION,
            "locator": infonet.get_locator(),
            "limit": page_limit,
        }
        try:
            payload = _peer_sync_response(peer_url, body)
        except PeerSyncRateLimited as exc:
            # Bubble up the retry-after so finish_sync can honor it.
            return False, str(exc), False, exc.retry_after_s
        if bool(payload.get("forked")):
            # Auto-recover small local forks: if the local chain is tiny
            # (< 20 events) and the remote has a longer chain, reset local
            # state and re-sync from genesis instead of failing forever.
            remote_count = int(payload.get("count", 0) or 0)
            local_count = len(infonet.events)
            if local_count < 20:
                logger.warning(
                    "Fork detected with small local chain (%d events). "
                    "Resetting to re-sync from peer (remote has %d events).",
                    local_count,
                    remote_count,
                )
                infonet.reset_chain()
                continue  # retry sync with clean genesis locator
            return False, "fork detected", True, 0
        events = payload.get("events", [])
        if not isinstance(events, list):
            return False, "peer sync events must be a list", False, 0
        if not events:
            return True, "", False, 0
        result = infonet.ingest_events(events)
        _hydrate_gate_store_from_chain(events)
        _hydrate_dm_relay_from_chain(events)
        rejected = list(result.get("rejected", []) or [])
        if rejected:
            reasons = [
                str((item or {}).get("reason", "") or "").strip()
                for item in rejected
                if isinstance(item, dict)
            ]
            reason_summary = ", ".join(reason for reason in reasons if reason)
            detail = f"sync ingest rejected {len(rejected)} event(s)"
            if reason_summary:
                detail = f"{detail}: {reason_summary}"
            local_empty = len(infonet.events) == 0
            stale_genesis = (
                local_empty
                and bool(events)
                and str((events[0] or {}).get("prev_hash", "") or "") == GENESIS_HASH
                and any("timestamp outside freshness window" in reason.lower() for reason in reasons)
            )
            if stale_genesis:
                detail = (
                    f"{detail}; peer appears to be serving an expired genesis chain. "
                    "Refresh or reset the peer chain, or perform an explicit one-time migration "
                    "with MESH_INGEST_EVENT_MAX_AGE_S=0."
                )
            return False, detail, False, 0
        if int(result.get("accepted", 0) or 0) == 0 and int(result.get("duplicates", 0) or 0) >= len(events):
            return True, "", False, 0
        if len(events) < page_limit:
            return True, "", False, 0
        rounds += 1
    return True, "", False, 0


def _run_public_sync_cycle() -> SyncWorkerState:
    from services.mesh.mesh_hashchain import infonet
    from services.mesh.mesh_peer_store import DEFAULT_PEER_STORE_PATH, PeerStore

    if not _participant_node_enabled():
        updated = _set_node_sync_disabled_state(current_head=infonet.head_hash)
        with _NODE_RUNTIME_LOCK:
            set_sync_state(updated)
        return updated

    store = PeerStore(DEFAULT_PEER_STORE_PATH)
    try:
        store.load()
    except Exception:
        store = PeerStore(DEFAULT_PEER_STORE_PATH)

    records = _filter_infonet_sync_records(store.records())
    peers = eligible_sync_peers(records, now=time.time())
    max_peers = max(1, int(getattr(get_settings(), "MESH_SYNC_MAX_PEERS_PER_CYCLE", 0) or 3))
    peers = peers[:max_peers]
    with _NODE_RUNTIME_LOCK:
        current_state = get_sync_state()
    if not peers:
        if _infonet_private_transport_required():
            updated = finish_sync(
                current_state,
                ok=False,
                error=_infonet_private_transport_error(),
                now=time.time(),
                current_head=infonet.head_hash,
                failure_backoff_s=int(get_settings().MESH_SYNC_FAILURE_BACKOFF_S or 60),
            )
        else:
            updated = finish_solo_sync(
                current_state,
                now=time.time(),
                current_head=infonet.head_hash,
                interval_s=int(get_settings().MESH_SYNC_INTERVAL_S or 300),
            )
        with _NODE_RUNTIME_LOCK:
            set_sync_state(updated)
        return updated

    if _infonet_private_transport_required() and any(
        str(getattr(record, "transport", "") or "").strip().lower() == "onion"
        for record in peers
    ):
        _ensure_infonet_private_transport_ready("sync")

    last_error = "sync failed"
    for record in peers:
        retry_after_s = 0
        http_status_code = 0
        started = begin_sync(
            current_state,
            peer_url=record.peer_url,
            current_head=infonet.head_hash,
            now=time.time(),
        )
        with _NODE_RUNTIME_LOCK:
            set_sync_state(started)
        try:
            ok, error, forked, retry_after_s = _sync_from_peer(record.peer_url)
        except PeerSyncHTTPError as exc:
            # _sync_from_peer catches PeerSyncRateLimited internally (4-tuple
            # path for 429 with Retry-After). Other non-200 statuses surface
            # here as PeerSyncHTTPError ΓÇö pull retry_after_s + status off it
            # so the cooldown calculation below can honor server hints even
            # for non-429 throttling responses.
            ok = False
            error = str(exc)
            forked = False
            retry_after_s = int(exc.retry_after_s or 0)
            http_status_code = int(exc.status_code or 0)
        except Exception as exc:
            ok = False
            error = str(exc or type(exc).__name__)
            forked = False
            retry_after_s = 0
        if ok:
            store.mark_seen(record.peer_url, "sync", now=time.time())
            store.mark_sync_success(record.peer_url, now=time.time())
            store.save()
            updated = finish_sync(
                started,
                ok=True,
                peer_url=record.peer_url,
                current_head=infonet.head_hash,
                now=time.time(),
                interval_s=int(get_settings().MESH_SYNC_INTERVAL_S or 300),
            )
            with _NODE_RUNTIME_LOCK:
                set_sync_state(updated)
            return updated

        last_error = error
        settings = get_settings()
        is_seed_peer = str(getattr(record, "role", "") or "").strip().lower() == "seed"
        cooldown_s = int(getattr(settings, "MESH_RELAY_FAILURE_COOLDOWN_S", 120) or 120)
        if is_seed_peer:
            cooldown_s = int(
                getattr(settings, "MESH_BOOTSTRAP_SEED_FAILURE_COOLDOWN_S", cooldown_s)
                or cooldown_s
            )
        if http_status_code == 429:
            failure_count = max(int(getattr(record, "failure_count", 0) or 0), current_state.consecutive_failures)
            exponential_429_s = min(900, 60 * (2 ** min(failure_count, 4)))
            cooldown_s = max(cooldown_s, retry_after_s, exponential_429_s)
        store.mark_failure(
            record.peer_url,
            "sync",
            error=error,
            cooldown_s=cooldown_s,
            now=time.time(),
        )
        store.save()
        failure_backoff_s = int(settings.MESH_SYNC_FAILURE_BACKOFF_S or 60)
        if is_seed_peer:
            failure_backoff_s = max(failure_backoff_s, max(1, cooldown_s))
        updated = finish_sync(
            started,
            ok=False,
            peer_url=record.peer_url,
            current_head=infonet.head_hash,
            error=error,
            fork_detected=forked,
            now=time.time(),
            interval_s=int(get_settings().MESH_SYNC_INTERVAL_S or 300),
            failure_backoff_s=failure_backoff_s,
            # 429 retry-storm fix: when the peer returned HTTP 429 with
            # a Retry-After header, finish_sync uses max(exponential,
            # retry_after) for next_sync_due_at ΓÇö so we actually wait
            # the time the upstream asked for instead of hammering
            # every 60s and keeping its rate-limit bucket full forever.
            retry_after_s=retry_after_s,
        )
        with _NODE_RUNTIME_LOCK:
            set_sync_state(updated)
        if forked:
            return updated
        current_state = updated

    return updated if peers else finish_sync(
        current_state,
        ok=False,
        error=last_error,
        now=time.time(),
        current_head=infonet.head_hash,
        failure_backoff_s=int(get_settings().MESH_SYNC_FAILURE_BACKOFF_S or 60),
    )


_NODE_SYNC_KICK_LOCK = threading.Lock()


def _kick_public_sync_background(reason: str = "") -> None:
    """Start one immediate Infonet sync attempt without waiting for the poll loop."""
    if not _node_runtime_supported() or not _participant_node_enabled():
        return

    def _runner() -> None:
        if not _NODE_SYNC_KICK_LOCK.acquire(blocking=False):
            return
        try:
            label = f" ({reason})" if reason else ""
            logger.info("Infonet sync kick starting%s", label)
            _run_public_sync_cycle()
        except Exception:
            logger.exception("Infonet sync kick failed")
        finally:
            _NODE_SYNC_KICK_LOCK.release()

    threading.Thread(
        target=_runner,
        daemon=True,
        name="infonet-sync-kick",
    ).start()


def _public_infonet_sync_loop() -> None:
    from services.mesh.mesh_hashchain import infonet

    while not _NODE_SYNC_STOP.is_set():
        try:
            if not _node_runtime_supported():
                _NODE_SYNC_STOP.wait(5.0)
                continue
            if not _participant_node_enabled():
                disabled = _set_node_sync_disabled_state(current_head=infonet.head_hash)
                with _NODE_RUNTIME_LOCK:
                    set_sync_state(disabled)
                _NODE_SYNC_STOP.wait(5.0)
                continue
            with _NODE_RUNTIME_LOCK:
                state = get_sync_state()
            if should_run_sync(state, now=time.time()):
                _run_public_sync_cycle()
        except Exception:
            logger.exception("public infonet sync loop failed")
        _NODE_SYNC_STOP.wait(5.0)


def _record_public_push_result(event_id: str, *, ok: bool, error: str = "", results: list[dict[str, Any]] | None = None) -> None:
    with _NODE_RUNTIME_LOCK:
        snapshot = {
            "last_event_id": str(event_id or ""),
            "last_push_ok_at": int(time.time()) if ok else int(_NODE_PUSH_STATE.get("last_push_ok_at", 0) or 0),
            "last_push_error": "" if ok else str(error or "").strip(),
            "last_results": list(results or []),
        }
        _NODE_PUSH_STATE.update(snapshot)


def _propagate_public_event_to_peers(event_dict: dict[str, Any]) -> None:
    from services.mesh.mesh_router import MeshEnvelope, mesh_router

    if not _participant_node_enabled():
        return
    if not _filter_infonet_peer_urls(authenticated_push_peer_urls()):
        return

    envelope = MeshEnvelope(
        sender_id=str(event_dict.get("node_id", "") or ""),
        destination="broadcast",
        payload=json_mod.dumps(event_dict, sort_keys=True, separators=(",", ":"), ensure_ascii=False),
        trust_tier="public_degraded",
    )
    results = []
    for transport in (mesh_router.internet, mesh_router.tor_arti):
        try:
            if transport.can_reach(envelope):
                result = transport.send(envelope, {})
                results.append(result.to_dict())
        except Exception as exc:
            results.append({"ok": False, "transport": getattr(transport, "NAME", "unknown"), "detail": type(exc).__name__})
    ok = any(bool(result.get("ok")) for result in results)
    _record_public_push_result(
        str(event_dict.get("event_id", "") or ""),
        ok=ok,
        error="" if ok else "all push peers failed",
        results=results,
    )


def _propagate_ledger_event_to_peers(event_dict: dict[str, Any]) -> None:
    if not _participant_node_enabled():
        return
    event_type = str(event_dict.get("event_type") or "")
    if event_type in {"gate_message", "dm_message"}:
        from services.mesh.mesh_swarm_runtime import push_infonet_events_to_http_peers

        push_infonet_events_to_http_peers([event_dict])
        _kick_public_sync_background("ledger_event")
        return
    _propagate_public_event_to_peers(event_dict)


def _schedule_public_event_propagation(event_dict: dict[str, Any]) -> None:
    threading.Thread(
        target=_propagate_ledger_event_to_peers,
        args=(dict(event_dict),),
        daemon=True,
    ).start()


def _infonet_node_runtime_requested() -> bool:
    return (not _MESH_ONLY) or _HEADLESS_MESH_NODE_RUNTIME


def _start_infonet_node_runtime(reason: str = "startup") -> None:
    """Start sync/push/pull workers for participant nodes."""
    global _NODE_PUBLIC_EVENT_HOOK_REGISTERED, _NODE_RUNTIME_THREADS_STARTED

    if not _infonet_node_runtime_requested():
        return
    try:
        from services.mesh.mesh_hashchain import register_public_event_append_hook

        _materialize_local_infonet_state()
        _refresh_node_peer_store()
        if _node_runtime_supported():
            if not _participant_node_enabled():
                logger.info("Infonet participant auto-enabled for private seed sync")
                _set_participant_node_enabled(True)
            threading.Thread(
                target=lambda: _ensure_infonet_private_transport_ready(reason),
                daemon=True,
                name="infonet-private-transport-warmup",
            ).start()
            _NODE_SYNC_STOP.clear()
            if not _NODE_RUNTIME_THREADS_STARTED:
                threading.Thread(target=_public_infonet_sync_loop, daemon=True).start()
                threading.Thread(target=_http_peer_push_loop, daemon=True).start()
                threading.Thread(target=_http_gate_push_loop, daemon=True).start()
                threading.Thread(target=_http_gate_pull_loop, daemon=True).start()
                threading.Thread(target=_swarm_manifest_pull_loop, daemon=True).start()
                _NODE_RUNTIME_THREADS_STARTED = True
            _kick_public_sync_background(reason)
        if not _NODE_PUBLIC_EVENT_HOOK_REGISTERED:
            register_public_event_append_hook(_schedule_public_event_propagation)
            _NODE_PUBLIC_EVENT_HOOK_REGISTERED = True
    except Exception as e:
        logger.warning(f"Node bootstrap runtime failed to initialize: {e}")


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Background HTTP Peer Push Worker ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
# Runs alongside the sync loop.  Every PUSH_INTERVAL seconds, batches new
# Infonet events and sends them via HMAC-authenticated POST to push peers.

_PEER_PUSH_INTERVAL_S = 10
_PEER_PUSH_BATCH_SIZE = 50
_peer_push_last_index: dict[str, int] = {}  # peer_url ├óΓÇáΓÇÖ last pushed event index
_INFONET_SYNC_RATE_LIMIT = "600/minute"


def _http_peer_push_loop() -> None:
    """Background thread: push new Infonet events to HTTP peers."""
    import requests as _requests
    from services.mesh.mesh_hashchain import infonet
    from services.mesh.mesh_peer_store import DEFAULT_PEER_STORE_PATH, PeerStore

    while not _NODE_SYNC_STOP.is_set():
        try:
            if not _participant_node_enabled():
                _NODE_SYNC_STOP.wait(_PEER_PUSH_INTERVAL_S)
                continue

            # Issue #256: resolve_peer_key_for_url() handles both the
            # legacy global MESH_PEER_PUSH_SECRET path and the per-peer
            # MESH_PEER_SECRETS map. The per-peer skip happens below
            # ("if not peer_key: continue"), so we don't gate the whole
            # loop on the global secret being set ΓÇö an install that only
            # configures per-peer secrets is now valid.

            peers = _filter_infonet_peer_urls(authenticated_push_peer_urls())
            if not peers:
                _NODE_SYNC_STOP.wait(_PEER_PUSH_INTERVAL_S)
                continue

            all_events = infonet.events
            total = len(all_events)

            for peer_url in peers:
                normalized = normalize_peer_url(peer_url)
                if not normalized:
                    continue
                last_idx = _peer_push_last_index.get(normalized, 0)
                if last_idx >= total:
                    continue  # nothing new

                batch = all_events[last_idx : last_idx + _PEER_PUSH_BATCH_SIZE]
                if not batch:
                    continue

                try:
                    body_bytes = json_mod.dumps(
                        {"events": batch},
                        sort_keys=True,
                        separators=(",", ":"),
                        ensure_ascii=False,
                    ).encode("utf-8")

                    sender_url = _local_infonet_peer_url()
                    peer_key = resolve_peer_key_for_url(sender_url)
                    if not peer_key:
                        continue
                    import hmac as _hmac_mod2
                    import hashlib as _hashlib_mod2
                    hmac_hex = _hmac_mod2.new(peer_key, body_bytes, _hashlib_mod2.sha256).hexdigest()

                    timeout = int(get_settings().MESH_RELAY_PUSH_TIMEOUT_S or 10)
                    proxies = _infonet_peer_requests_proxies(normalized)
                    request_kwargs: dict[str, Any] = {
                        "data": body_bytes,
                        "headers": {
                            "Content-Type": "application/json",
                            "X-Peer-Url": sender_url,
                            "X-Peer-HMAC": hmac_hex,
                        },
                        "timeout": timeout,
                    }
                    if proxies:
                        request_kwargs["proxies"] = proxies
                    resp = _requests.post(
                        f"{normalized}/api/mesh/infonet/peer-push",
                        **request_kwargs,
                    )
                    if resp.status_code == 200:
                        _peer_push_last_index[normalized] = last_idx + len(batch)
                        logger.info(
                            f"Pushed {len(batch)} event(s) to {normalized[:40]} "
                            f"(idx {last_idx}├óΓÇáΓÇÖ{last_idx + len(batch)})"
                        )
                    else:
                        logger.warning(f"Peer push to {normalized[:40]} returned {resp.status_code}")
                except Exception as exc:
                    logger.warning(f"Peer push to {normalized[:40]} failed: {exc}")

        except Exception:
            logger.exception("HTTP peer push loop error")
        _NODE_SYNC_STOP.wait(_PEER_PUSH_INTERVAL_S)


def _swarm_manifest_pull_loop() -> None:
    """Background thread: pull signed peer manifests from bootstrap seeds."""
    while not _NODE_SYNC_STOP.is_set():
        try:
            if _participant_node_enabled():
                from services.mesh.mesh_swarm_runtime import refresh_swarm_manifest_from_seeds

                result = refresh_swarm_manifest_from_seeds()
                if result.get("ok") and not result.get("skipped"):
                    _refresh_node_peer_store()
        except Exception:
            logger.exception("swarm manifest pull loop error")
        interval_s = int(getattr(get_settings(), "MESH_SWARM_MANIFEST_PULL_INTERVAL_S", 0) or 300)
        _NODE_SYNC_STOP.wait(max(30, interval_s))


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Background Gate Message Pull Worker ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
# Periodically pulls gate events from relay peers that this node is missing.
# Complements the push loop: push sends OUR events to peers, pull fetches
# THEIR events from peers (needed when this node is behind NAT).

_GATE_PULL_INTERVAL_S = 10
_gate_pull_last_count: dict[str, dict[str, int]] = {}  # peer ├óΓÇáΓÇÖ {gate_id ├óΓÇáΓÇÖ known count}


def _http_gate_pull_loop() -> None:
    """Background thread: pull new gate messages from HTTP relay peers."""
    import requests as _requests
    from services.mesh.mesh_hashchain import gate_store

    while not _NODE_SYNC_STOP.is_set():
        try:
            if not _participant_node_enabled():
                _NODE_SYNC_STOP.wait(_GATE_PULL_INTERVAL_S)
                continue

            # Issue #256: per-peer key resolution; see _http_peer_push_loop.

            peers = _filter_infonet_peer_urls(authenticated_push_peer_urls())
            if not peers:
                _NODE_SYNC_STOP.wait(_GATE_PULL_INTERVAL_S)
                continue

            for peer_url in peers:
                normalized = normalize_peer_url(peer_url)
                if not normalized:
                    continue

                sender_url = _local_infonet_peer_url()
                peer_key = resolve_peer_key_for_url(sender_url)
                if not peer_key:
                    continue

                peer_counts = _gate_pull_last_count.setdefault(normalized, {})

                try:
                    # Step 1: Ask the peer which gates it has and how many events each
                    discovery_body = json_mod.dumps(
                        {"gate_id": "", "after_count": 0},
                        sort_keys=True,
                        separators=(",", ":"),
                        ensure_ascii=False,
                    ).encode("utf-8")

                    import hmac as _hmac_pull
                    import hashlib as _hashlib_pull
                    discovery_hmac = _hmac_pull.new(peer_key, discovery_body, _hashlib_pull.sha256).hexdigest()

                    timeout = int(get_settings().MESH_RELAY_PUSH_TIMEOUT_S or 10)
                    proxies = _infonet_peer_requests_proxies(normalized)
                    discovery_kwargs: dict[str, Any] = {
                        "data": discovery_body,
                        "headers": {
                            "Content-Type": "application/json",
                            "X-Peer-Url": sender_url,
                            "X-Peer-HMAC": discovery_hmac,
                        },
                        "timeout": timeout,
                    }
                    if proxies:
                        discovery_kwargs["proxies"] = proxies
                    resp = _requests.post(
                        f"{normalized}/api/mesh/gate/peer-pull",
                        **discovery_kwargs,
                    )
                    if resp.status_code != 200:
                        continue
                    discovery = resp.json()
                    if not discovery.get("ok"):
                        continue
                    remote_gates: dict[str, int] = discovery.get("gates", {})
                    if not remote_gates:
                        continue

                    # Step 2: For each gate with new events, pull the batch
                    for gate_id, remote_total in remote_gates.items():
                        local_known = peer_counts.get(gate_id, 0)
                        # Also account for what we already have locally
                        with gate_store._lock:
                            local_count = len(gate_store._gates.get(gate_id, []))
                        effective_cursor = max(local_known, local_count)
                        if effective_cursor >= remote_total:
                            continue

                        pull_body = json_mod.dumps(
                            {"gate_id": gate_id, "after_count": effective_cursor},
                            sort_keys=True,
                            separators=(",", ":"),
                            ensure_ascii=False,
                        ).encode("utf-8")

                        pull_hmac = _hmac_pull.new(peer_key, pull_body, _hashlib_pull.sha256).hexdigest()

                        pull_kwargs: dict[str, Any] = {
                            "data": pull_body,
                            "headers": {
                                "Content-Type": "application/json",
                                "X-Peer-Url": sender_url,
                                "X-Peer-HMAC": pull_hmac,
                            },
                            "timeout": timeout,
                        }
                        if proxies:
                            pull_kwargs["proxies"] = proxies
                        pull_resp = _requests.post(
                            f"{normalized}/api/mesh/gate/peer-pull",
                            **pull_kwargs,
                        )
                        if pull_resp.status_code != 200:
                            continue
                        pull_data = pull_resp.json()
                        if not pull_data.get("ok"):
                            continue

                        events = pull_data.get("events", [])
                        if not events:
                            peer_counts[gate_id] = remote_total
                            continue

                        result = gate_store.ingest_peer_events(gate_id, events)
                        accepted = int(result.get("accepted", 0) or 0)
                        dups = int(result.get("duplicates", 0) or 0)
                        if accepted > 0:
                            logger.info(
                                "Gate pull: %d new event(s) for %s from %s",
                                accepted, gate_id[:12], normalized[:40],
                            )
                        peer_counts[gate_id] = effective_cursor + len(events)

                except Exception as exc:
                    logger.warning("Gate pull from %s failed: %s", normalized[:40], exc)

        except Exception:
            logger.exception("HTTP gate pull loop error")
        _NODE_SYNC_STOP.wait(_GATE_PULL_INTERVAL_S)




# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Background Gate Message Push Worker ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼

_gate_push_last_count: dict[str, dict[str, int]] = {}  # peer ├óΓÇáΓÇÖ {gate_id ├óΓÇáΓÇÖ count}


def _http_gate_push_loop() -> None:
    """Background thread: push new gate messages to HTTP peers."""
    import requests as _requests
    from services.mesh.mesh_hashchain import gate_store

    while not _NODE_SYNC_STOP.is_set():
        try:
            if not _participant_node_enabled():
                _NODE_SYNC_STOP.wait(_PEER_PUSH_INTERVAL_S)
                continue

            # Issue #256: per-peer key resolution; see _http_peer_push_loop.

            peers = _filter_infonet_peer_urls(authenticated_push_peer_urls())
            if not peers:
                _NODE_SYNC_STOP.wait(_PEER_PUSH_INTERVAL_S)
                continue

            with gate_store._lock:
                gate_ids = list(gate_store._gates.keys())

            for peer_url in peers:
                normalized = normalize_peer_url(peer_url)
                if not normalized:
                    continue

                sender_url = _local_infonet_peer_url()
                peer_key = resolve_peer_key_for_url(sender_url)
                if not peer_key:
                    continue

                peer_counts = _gate_push_last_count.setdefault(normalized, {})

                for gate_id in gate_ids:
                    with gate_store._lock:
                        all_events = list(gate_store._gates.get(gate_id, []))
                    total = len(all_events)
                    last = peer_counts.get(gate_id, 0)
                    if last >= total:
                        continue

                    batch = all_events[last : last + _PEER_PUSH_BATCH_SIZE]
                    if not batch:
                        continue

                    try:
                        body_bytes = json_mod.dumps(
                            {"events": batch},
                            sort_keys=True,
                            separators=(",", ":"),
                            ensure_ascii=False,
                        ).encode("utf-8")

                        import hmac as _hmac_mod3
                        import hashlib as _hashlib_mod3
                        hmac_hex = _hmac_mod3.new(peer_key, body_bytes, _hashlib_mod3.sha256).hexdigest()

                        timeout = int(get_settings().MESH_RELAY_PUSH_TIMEOUT_S or 10)
                        proxies = _infonet_peer_requests_proxies(normalized)
                        request_kwargs: dict[str, Any] = {
                            "data": body_bytes,
                            "headers": {
                                "Content-Type": "application/json",
                                "X-Peer-Url": sender_url,
                                "X-Peer-HMAC": hmac_hex,
                            },
                            "timeout": timeout,
                        }
                        if proxies:
                            request_kwargs["proxies"] = proxies
                        resp = _requests.post(
                            f"{normalized}/api/mesh/gate/peer-push",
                            **request_kwargs,
                        )
                        if resp.status_code == 200:
                            peer_counts[gate_id] = last + len(batch)
                            logger.info(
                                f"Gate push: {len(batch)} event(s) for {gate_id[:12]} "
                                f"to {normalized[:40]}"
                            )
                        else:
                            logger.warning(
                                f"Gate push to {normalized[:40]} returned {resp.status_code}"
                            )
                    except Exception as exc:
                        logger.warning(f"Gate push to {normalized[:40]} failed: {exc}")

        except Exception:
            logger.exception("HTTP gate push loop error")
        _NODE_SYNC_STOP.wait(_PEER_PUSH_INTERVAL_S)


def _redacted_gate_timestamp(event: dict[str, Any]) -> float:
    raw_ts = float((event or {}).get("timestamp", 0) or 0.0)
    if raw_ts <= 0:
        return 0.0
    try:
        jitter_window = max(0, int(get_settings().MESH_GATE_TIMESTAMP_JITTER_S or 0))
    except Exception:
        jitter_window = 0
    if jitter_window <= 0:
        return raw_ts
    event_id = str((event or {}).get("event_id", "") or "")
    seed = _hashlib_mod.sha256(f"{event_id}|{int(raw_ts)}".encode("utf-8")).digest()
    fraction = int.from_bytes(seed[:8], "big") / float(2**64 - 1)
    return max(0.0, raw_ts - (fraction * float(jitter_window)))


def _redact_wormhole_settings(settings: dict[str, Any], authenticated: bool) -> dict[str, Any]:
    if authenticated:
        return dict(settings)
    return {
        key: settings.get(key)
        for key in _WORMHOLE_PUBLIC_SETTINGS_FIELDS
        if key in settings
    }


def _redact_privacy_profile_settings(
    settings: dict[str, Any],
    authenticated: bool,
) -> dict[str, Any]:
    profile = {
        "profile": settings.get("privacy_profile", "default"),
        "wormhole_enabled": bool(settings.get("enabled")),
        "transport": settings.get("transport", "direct"),
        "anonymous_mode": bool(settings.get("anonymous_mode")),
    }
    if authenticated:
        return profile
    return {
        key: profile.get(key)
        for key in _WORMHOLE_PUBLIC_PROFILE_FIELDS
    }


def _redact_private_lane_control_fields(
    payload: dict[str, Any],
    authenticated: bool,
) -> dict[str, Any]:
    redacted = dict(payload)
    if authenticated:
        return redacted
    for field in _PRIVATE_LANE_CONTROL_FIELDS:
        redacted.pop(field, None)
    return redacted


def _redact_public_rns_status(
    payload: dict[str, Any],
    authenticated: bool,
) -> dict[str, Any]:
    redacted = _redact_private_lane_control_fields(payload, authenticated=authenticated)
    if authenticated:
        return redacted
    return {
        key: redacted.get(key)
        for key in _PUBLIC_RNS_STATUS_FIELDS
        if key in redacted
    }


def _redact_public_mesh_status(
    payload: dict[str, Any],
    authenticated: bool,
) -> dict[str, Any]:
    if authenticated:
        return dict(payload)
    return {
        "message_log_size": int(payload.get("message_log_size", 0) or 0),
    }


def _redact_public_oracle_profile(
    payload: dict[str, Any],
    authenticated: bool,
) -> dict[str, Any]:
    redacted = dict(payload)
    if authenticated:
        return redacted
    redacted["active_stakes"] = []
    redacted["prediction_history"] = []
    return redacted


def _redact_public_oracle_predictions(
    predictions: list[dict[str, Any]],
    authenticated: bool,
) -> dict[str, Any]:
    if authenticated:
        return {"predictions": list(predictions)}
    return {
        "predictions": [],
        "count": len(predictions),
    }


def _redact_public_oracle_stakes(
    payload: dict[str, Any],
    authenticated: bool,
) -> dict[str, Any]:
    redacted = dict(payload)
    if authenticated:
        return redacted
    redacted["truth_stakers"] = []
    redacted["false_stakers"] = []
    return redacted


def _redact_public_node_history(
    events: list[dict[str, Any]],
    authenticated: bool,
) -> list[dict[str, Any]]:
    if authenticated:
        return [dict(event) for event in events]
    return [
        {
            "event_id": str(event.get("event_id", "") or ""),
            "event_type": str(event.get("event_type", "") or ""),
            "timestamp": float(event.get("timestamp", 0) or 0),
        }
        for event in events
    ]


def _redact_composed_gate_message(payload: dict[str, Any]) -> dict[str, Any]:
    safe = {
        "ok": bool(payload.get("ok")),
        "gate_id": str(payload.get("gate_id", "") or ""),
        "identity_scope": str(payload.get("identity_scope", "") or ""),
        "ciphertext": str(payload.get("ciphertext", "") or ""),
        "nonce": str(payload.get("nonce", "") or ""),
        "sender_ref": str(payload.get("sender_ref", "") or ""),
        "format": str(payload.get("format", "mls1") or "mls1"),
        "transport_lock": str(payload.get("transport_lock", "") or ""),
        "timestamp": float(payload.get("timestamp", 0) or 0),
    }
    epoch = payload.get("epoch", 0)
    if epoch:
        safe["epoch"] = int(epoch or 0)
    if payload.get("reply_to"):
        safe["reply_to"] = str(payload.get("reply_to", "") or "")
    if payload.get("detail"):
        safe["detail"] = str(payload.get("detail", "") or "")
    if payload.get("key_commitment"):
        safe["key_commitment"] = str(payload.get("key_commitment", "") or "")
    if payload.get("gate_envelope"):
        safe["gate_envelope"] = str(payload.get("gate_envelope", "") or "")
    if payload.get("envelope_hash"):
        safe["envelope_hash"] = str(payload.get("envelope_hash", "") or "")
    return safe


def _redact_signed_gate_message(payload: dict[str, Any]) -> dict[str, Any]:
    safe = {
        "ok": bool(payload.get("ok")),
        "gate_id": str(payload.get("gate_id", "") or ""),
        "identity_scope": str(payload.get("identity_scope", "") or ""),
        "sender_id": str(payload.get("sender_id", "") or ""),
        "public_key": str(payload.get("public_key", "") or ""),
        "public_key_algo": str(payload.get("public_key_algo", "") or ""),
        "protocol_version": str(payload.get("protocol_version", "") or ""),
        "sequence": int(payload.get("sequence", 0) or 0),
        "ciphertext": str(payload.get("ciphertext", "") or ""),
        "nonce": str(payload.get("nonce", "") or ""),
        "sender_ref": str(payload.get("sender_ref", "") or ""),
        "format": str(payload.get("format", "mls1") or "mls1"),
        "timestamp": float(payload.get("timestamp", 0) or 0),
        "signature": str(payload.get("signature", "") or ""),
    }
    epoch = payload.get("epoch", 0)
    if epoch:
        safe["epoch"] = int(epoch or 0)
    if payload.get("reply_to"):
        safe["reply_to"] = str(payload.get("reply_to", "") or "")
    if payload.get("detail"):
        safe["detail"] = str(payload.get("detail", "") or "")
    if payload.get("gate_envelope"):
        safe["gate_envelope"] = str(payload.get("gate_envelope", "") or "")
    if payload.get("envelope_hash"):
        safe["envelope_hash"] = str(payload.get("envelope_hash", "") or "")
    return safe


def _build_cors_origins():
    """Build a CORS origins whitelist: localhost + LAN IPs + env overrides.
    Falls back to wildcard only if auto-detection fails entirely."""
    origins = [
        "http://localhost:3000",
        "http://127.0.0.1:3000",
        "http://localhost:8000",
        "http://127.0.0.1:8000",
    ]
    # Add this machine's LAN IPs (covers common home/office setups)
    try:
        hostname = socket.gethostname()
        for info in socket.getaddrinfo(hostname, None, socket.AF_INET):
            ip = info[4][0]
            if ip not in ("127.0.0.1", "0.0.0.0"):
                origins.append(f"http://{ip}:3000")
                origins.append(f"http://{ip}:8000")
    except Exception:
        pass
    # Allow user override via CORS_ORIGINS env var (comma-separated)
    extra = os.environ.get("CORS_ORIGINS", "")
    if extra:
        origins.extend([o.strip() for o in extra.split(",") if o.strip()])
    return list(set(origins))  # deduplicate


def _safe_int(val, default=0):
    try:
        return int(val)
    except (TypeError, ValueError):
        return default


def _safe_float(val, default=0.0):
    try:
        parsed = float(val)
        if not math.isfinite(parsed):
            return default
        return parsed
    except (TypeError, ValueError):
        return default


@asynccontextmanager
async def lifespan(app: FastAPI):
    _validate_insecure_admin_startup()
    _validate_admin_startup()
    _validate_peer_push_secret()
    _validate_privacy_core_startup()

    # Validate environment variables before starting anything
    from services.env_check import validate_env

    validate_env(strict=not _MESH_ONLY)

    if _MESH_ONLY:
        logger.info("MESH_ONLY enabled ├óΓé¼ΓÇ¥ skipping global data fetchers/schedulers.")
    else:
        # Start AIS stream first ├óΓé¼ΓÇ¥ it loads the disk cache (instant ships) then
        # begins accumulating live vessel data via WebSocket in the background.
        start_ais_stream()

        # Carrier tracker runs its own initial update_carrier_positions() internally
        # in _scheduler_loop, so we do NOT call it again in the preload thread.
        start_carrier_tracker()

        # Start SIGINT grid eagerly ├óΓé¼ΓÇ¥ APRS-IS TCP + Meshtastic MQTT connections
        # take a few seconds to handshake and start receiving packets. By starting
        # now, the bridges are already accumulating signals by the time the first
        # fetch_sigint() reads them during the preload cycle.
        from services.sigint_bridge import sigint_grid

        sigint_grid.start()

    # Start Reticulum bridge (optional)
    try:
        from services.mesh.mesh_rns import rns_bridge

        rns_bridge.start()
    except Exception as e:
        logger.warning(f"RNS bridge failed to start: {e}")

    # Start periodic Infonet verifier
    def _verify_loop():
        from services.mesh.mesh_hashchain import infonet

        while True:
            try:
                interval = int(get_settings().MESH_VERIFY_INTERVAL_S or 0)
                if interval <= 0:
                    time.sleep(30)
                    continue
                valid, reason = infonet.validate_chain_incremental(verify_signatures=True)
                if not valid:
                    logger.error(f"Infonet validation failed: {reason}")
                    try:
                        from services.mesh.mesh_metrics import increment as metrics_inc

                        metrics_inc("infonet_validate_failed")
                    except Exception:
                        pass
                time.sleep(max(5, interval))
            except Exception:
                time.sleep(30)

    threading.Thread(target=_verify_loop, daemon=True).start()

    # Only the primary backend supervises Wormhole. The Wormhole process itself
    # runs this same app in MESH_ONLY mode and must not recurse into spawning.
    if not _MESH_ONLY:
        def _startup_wormhole_runtime():
            try:
                from services.mesh.mesh_infonet_relay_bootstrap import ensure_infonet_relay_wormhole_ready
                from services.wormhole_supervisor import get_wormhole_state, sync_wormhole_with_settings

                ensure_infonet_relay_wormhole_ready(reason="startup_relay")
                sync_wormhole_with_settings()
                _resume_private_delivery_background_work(
                    current_tier=_current_private_lane_tier(get_wormhole_state()),
                    reason="startup_resume",
                )
                _refresh_lookup_handle_rotation_background(reason="startup_resume")
                privacy_prewarm_service.ensure_started()
                privacy_prewarm_service.run_scheduled_once(reason="startup_resume")
            except Exception as e:
                logger.warning(f"Wormhole supervisor failed to sync: {e}")

        threading.Thread(
            target=_startup_wormhole_runtime,
            daemon=True,
            name="wormhole-startup-sync",
        ).start()

    _start_infonet_node_runtime("startup")

    if not _MESH_ONLY:
        # Prime the static route/airport database from vrs-standing-data.adsb.lol
        # before the first flight fetch so callsigns resolve to origin/destination
        # immediately. Daily refresh is owned by the scheduler.
        def _prime_route_database():
            try:
                from services.fetchers.route_database import refresh_route_database
                refresh_route_database(force=True)
            except Exception as e:
                logger.warning(f"Route database prime failed (non-fatal): {e}")

        threading.Thread(target=_prime_route_database, daemon=True).start()

        # Prime the OpenSky aircraft metadata DB so hex24 -> aircraft type
        # lookups work on the first flight cycle (and emissions get populated
        # for OpenSky-sourced flights that arrive with no t field).
        def _prime_aircraft_database():
            try:
                from services.fetchers.aircraft_database import refresh_aircraft_database
                refresh_aircraft_database(force=True)
            except Exception as e:
                logger.warning(f"Aircraft database prime failed (non-fatal): {e}")

        threading.Thread(target=_prime_aircraft_database, daemon=True).start()

        # Seed cached first-paint layers before accepting requests. This is
        # disk-only and keeps the critical bootstrap endpoint independent from
        # slow network warmup.
        seed_startup_caches()

        # Start the recurring scheduler (fast=60s, slow=30min).
        start_scheduler()

        # Kick off the full data preload in a background thread so the server
        # is listening on port 8000 instantly.  The frontend's adaptive polling
        # (retries every 3s) will pick up data piecemeal as each fetcher finishes.
        def _background_preload():
            delay_s = float(os.environ.get("SHADOWBROKER_STARTUP_PRELOAD_DELAY_S", "2.0") or 0)
            if delay_s > 0:
                time.sleep(delay_s)
            logger.info("=== PRELOADING DATA (background ├óΓé¼ΓÇ¥ server already accepting requests) ===")
            try:
                update_all_data(startup_mode=True)
                logger.info("=== PRELOAD COMPLETE ===")
            except Exception as e:
                logger.error(f"Data preload failed (non-fatal): {e}")

        threading.Thread(target=_background_preload, daemon=True).start()

    # Auto-restart Tor hidden service if it was previously running
    # (i.e., the hostname file exists from a previous session)
    try:
        from services.tor_hidden_service import tor_service, HOSTNAME_PATH
        if HOSTNAME_PATH.exists():
            logger.info("Previous Tor hidden service detected ├óΓé¼ΓÇ¥ auto-restarting...")
            threading.Thread(
                target=tor_service.start, daemon=True
            ).start()
    except Exception as e:
        logger.warning(f"Tor auto-restart failed (non-fatal): {e}")

    yield
    if not _MESH_ONLY:
        # Shutdown: Stop all background services
        _NODE_SYNC_STOP.set()
        stop_ais_stream()
        stop_scheduler()
        stop_carrier_tracker()
        try:
            sigint_grid.stop()
        except Exception:
            pass
    if not _MESH_ONLY:
        try:
            from services.wormhole_supervisor import shutdown_wormhole_supervisor

            shutdown_wormhole_supervisor()
        except Exception:
            pass
    try:
        privacy_prewarm_service.stop()
    except Exception:
        pass
    # Stop Tor hidden service subprocess
    try:
        from services.tor_hidden_service import tor_service
        tor_service.stop()
    except Exception:
        pass


app = FastAPI(title="Live Risk Dashboard API", lifespan=lifespan)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)


@app.exception_handler(JSONDecodeError)
async def json_decode_error_handler(_request: Request, _exc: JSONDecodeError):
    return JSONResponse(status_code=422, content={"ok": False, "detail": "invalid JSON body"})


@app.exception_handler(StarletteHTTPException)
async def private_plane_http_exception_handler(request: Request, exc: StarletteHTTPException):
    if exc.status_code == 403 and _is_private_plane_access_path(_request_scope_path(request), request.method):
        return await _private_plane_refusal_response(
            request,
            status_code=403,
            payload=_private_plane_access_denied_payload(),
        )
    return await fastapi_http_exception_handler(request, exc)

from fastapi.middleware.gzip import GZipMiddleware

app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(
    CORSMiddleware,
    allow_origins=_build_cors_origins(),
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

_NO_STORE_HEADERS = {
    "Cache-Control": "no-store, max-age=0",
    "Pragma": "no-cache",
}
@app.middleware("http")
async def mesh_security_headers(request: Request, call_next):
    response = await call_next(request)
    for header, value in _security_headers().items():
        response.headers.setdefault(header, value)
    return response


@app.middleware("http")
async def mesh_no_store_headers(request: Request, call_next):
    response = await call_next(request)
    if _request_scope_path(request).startswith("/api/mesh/"):
        response.headers["Cache-Control"] = "no-store, max-age=0"
        response.headers["Pragma"] = "no-cache"
    return response


def _validate_gate_vote_context(voter_id: str, gate_id: str) -> tuple[bool, str]:
    gate_key = str(gate_id or "").strip().lower()
    if not gate_key:
        return True, ""
    try:
        from services.mesh.mesh_reputation import gate_manager
    except Exception as exc:
        return False, f"Gate validation unavailable: {exc}"

    gate = gate_manager.get_gate(gate_key)
    if not gate:
        return False, f"Gate '{gate_key}' does not exist"

    can_enter, reason = gate_manager.can_enter(voter_id, gate_key)
    if not can_enter:
        return False, f"Gate vote denied: {reason}"

    try:
        from services.mesh.mesh_hashchain import gate_store

        if not gate_store.get_messages(gate_key, limit=1):
            return False, f"Gate '{gate_key}' has no activity"
    except Exception:
        pass

    return True, gate_key


_GATE_REDACT_FIELDS = ("sender_ref", "epoch", "nonce")
_KEY_ROTATE_REDACT_FIELDS = {
    "old_node_id",
    "old_public_key",
    "old_public_key_algo",
    "old_signature",
}


def _redact_gate_metadata(event: dict) -> dict:
    """Strip MLS-internal fields from gate_message events in public sync responses."""
    if not isinstance(event, dict):
        return event
    event_type = str(event.get("event_type", "") or "")
    if event_type != "gate_message":
        return event
    redacted = dict(event)
    for field in ("node_id", "sequence"):
        redacted.pop(field, None)
    if isinstance(redacted.get("payload"), dict):
        payload = dict(redacted.get("payload") or {})
        for field in _GATE_REDACT_FIELDS:
            payload.pop(field, None)
        redacted["payload"] = payload
        return redacted
    for field in _GATE_REDACT_FIELDS:
        redacted.pop(field, None)
    return redacted


def _redact_key_rotate_payload(event: dict) -> dict:
    """Strip identity-linking fields from key_rotate events in public responses."""
    if not isinstance(event, dict):
        return event
    if str(event.get("event_type", "") or "") != "key_rotate":
        return event
    redacted = dict(event)
    payload = redacted.get("payload")
    if isinstance(payload, dict):
        payload = dict(payload)
        for field in _KEY_ROTATE_REDACT_FIELDS:
            payload.pop(field, None)
        redacted["payload"] = payload
    return redacted


def _redact_vote_gate(event: dict) -> dict:
    """Strip gate label from vote events in public responses."""
    if not isinstance(event, dict):
        return event
    if str(event.get("event_type", "") or "") != "vote":
        return event
    redacted = dict(event)
    payload = redacted.get("payload")
    if isinstance(payload, dict):
        payload = dict(payload)
        payload.pop("gate", None)
        redacted["payload"] = payload
    return redacted


def _redact_public_event(event: dict) -> dict:
    """Apply all public-response redactions for public chain endpoints."""
    return _redact_vote_gate(_redact_key_rotate_payload(_redact_gate_metadata(event)))


def _is_loopback_host(host: str) -> bool:
    value = str(host or "").strip().lower()
    if not value:
        return False
    if value.startswith("[") and "]" in value:
        value = value[1 : value.index("]")]
    if ":" in value and value.count(":") == 1:
        value = value.rsplit(":", 1)[0]
    if value in {"localhost", "ip6-localhost"}:
        return True
    try:
        return ipaddress.ip_address(value).is_loopback
    except ValueError:
        return False


def _is_onion_host(host: str) -> bool:
    value = str(host or "").strip().lower()
    if not value:
        return False
    if ":" in value and value.count(":") == 1:
        value = value.rsplit(":", 1)[0]
    return value.endswith(".onion")


def _forwarded_for_hosts(request) -> list[str]:
    headers = getattr(request, "headers", {}) or {}
    hosts: list[str] = []
    x_forwarded_for = str(headers.get("x-forwarded-for", "") or "")
    hosts.extend(part.strip() for part in x_forwarded_for.split(",") if part.strip())
    forwarded = str(headers.get("forwarded", "") or "")
    for section in forwarded.split(","):
        for item in section.split(";"):
            key, sep, value = item.strip().partition("=")
            if sep and key.strip().lower() == "for":
                hosts.append(value.strip().strip('"').strip("[]"))
    return hosts


def _request_appears_private_infonet_transport(request) -> bool:
    """Return whether a sync request is safe to carry private ledger events.

    This is intentionally fail-closed for the private event surface only. A
    questionable request still gets public events; gate/DM ciphertext simply
    stays out of the response.
    """
    if not _infonet_private_transport_required() or request is None:
        return False

    client = getattr(request, "client", None)
    client_host = str(getattr(client, "host", "") or "")
    if not (_is_loopback_host(client_host) or _is_onion_host(client_host)):
        return False

    forwarded_hosts = _forwarded_for_hosts(request)
    if forwarded_hosts and any(not (_is_loopback_host(host) or _is_onion_host(host)) for host in forwarded_hosts):
        return False

    return True


def _infonet_sync_response_events(events: list[dict], request=None) -> list[dict]:
    """Build the sync event surface for the current transport policy."""
    include_private = _request_appears_private_infonet_transport(request)
    response: list[dict] = []
    for event in events:
        if not isinstance(event, dict):
            continue
        event_type = str(event.get("event_type", "") or "")
        if event_type in {"gate_message", "dm_message"}:
            if include_private:
                response.append(dict(event))
            continue
        response.append(_redact_public_event(event))
    return response


def _trusted_gate_reply_to(event: dict) -> str:
    if not isinstance(event, dict):
        return ""
    payload = event.get("payload")
    if not isinstance(payload, dict):
        return ""
    reply_to = str(payload.get("reply_to", "") or "").strip()
    if not reply_to:
        return ""
    gate_id = str(payload.get("gate", "") or "").strip()
    node_id = str(event.get("node_id", "") or "").strip()
    public_key = str(event.get("public_key", "") or "").strip()
    public_key_algo = str(event.get("public_key_algo", "") or "").strip()
    if node_id and not public_key and gate_id:
        try:
            binding = _lookup_gate_member_binding(gate_id, node_id)
            if binding:
                public_key, public_key_algo = binding
        except Exception:
            return ""
    signature = str(event.get("signature", "") or "").strip()
    protocol_version = str(event.get("protocol_version", "") or "").strip()
    sequence = int(event.get("sequence", 0) or 0)
    if not (gate_id and node_id and public_key and public_key_algo and signature and protocol_version and sequence > 0):
        return ""
    verify_payload = {
        "gate": gate_id,
        "ciphertext": str(payload.get("ciphertext", "") or ""),
        "nonce": str(payload.get("nonce", "") or ""),
        "sender_ref": str(payload.get("sender_ref", "") or ""),
        "format": str(payload.get("format", "mls1") or "mls1"),
    }
    epoch = _safe_int(payload.get("epoch", 0) or 0)
    if epoch > 0:
        verify_payload["epoch"] = epoch
    envelope_hash = str(payload.get("envelope_hash", "") or "").strip()
    if envelope_hash:
        verify_payload["envelope_hash"] = envelope_hash
    return _recover_verified_gate_reply_to(
        node_id=node_id,
        sequence=sequence,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        payload=verify_payload,
        reply_to=reply_to,
        protocol_version=protocol_version,
    )


def _derive_anon_handle(node_id: str, gate_id: str) -> str:
    """Derive a stable per-session, per-gate anonymous display handle.

    Same node_id + same gate ΓåÆ same handle for every message that session
    posts (lets other members follow a conversation thread). Different
    session (anon re-enters ΓåÆ new node_id) ΓåÆ new handle. Different gate ΓåÆ
    different handle for the same session (prevents cross-gate linking).
    Not reversible: the handle is HMAC-SHA256(node_id, gate_id) truncated
    to 4 hex chars (~16 bits), which is enough to tell sessions apart in
    a room without identifying them.
    """
    node_key = str(node_id or "").strip()
    gate_key = str(gate_id or "").strip().lower()
    if not node_key:
        return "anon_????"
    tag = hmac.new(
        node_key.encode("utf-8"),
        f"{gate_key}|sender-handle-v1".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()[:4]
    return f"anon_{tag}"


def _strip_gate_identity_member(event: dict, *, envelope_policy: str = "envelope_disabled") -> dict:
    """Narrowed member view: strips signer identity fields.

    Gate envelope ciphertext is intentionally retained for members. It is
    encrypted under gate_secret and is required for durable room history.
    """
    if not isinstance(event, dict):
        event = {}
    payload = event.get("payload")
    if not isinstance(payload, dict):
        payload = {}
    gate_id = str(payload.get("gate", "") or "")
    sender_handle = _derive_anon_handle(str(event.get("node_id", "") or ""), gate_id)
    result_payload: dict = {
        "gate": gate_id,
        "ciphertext": str(payload.get("ciphertext", "") or ""),
        "format": str(payload.get("format", "") or ""),
        "nonce": str(payload.get("nonce", "") or ""),
        "sender_ref": str(payload.get("sender_ref", "") or ""),
        "sender_handle": sender_handle,
        "transport_lock": str(payload.get("transport_lock", "") or ""),
        # gate_envelope is AES-256-GCM ciphertext encrypted under the gate's
        # domain key (gate_secret). Only members who hold the gate_secret
        # can decrypt it ΓÇö so exposing the ciphertext itself to members is
        # safe, and it's REQUIRED for the envelope_always decrypt path that
        # gives members durable re-readable history. envelope_hash is the
        # cryptographic binding (SHA-256 of gate_envelope) the decrypt path
        # verifies before trusting the envelope.
        "gate_envelope": str(payload.get("gate_envelope", "") or ""),
        "envelope_hash": str(payload.get("envelope_hash", "") or ""),
        "reply_to": _trusted_gate_reply_to(event),
    }
    return {
        "event_id": str(event.get("event_id", "") or ""),
        "event_type": "gate_message",
        "timestamp": _redacted_gate_timestamp(event),
        "protocol_version": str(event.get("protocol_version", "") or ""),
        "sender_handle": sender_handle,
        "payload": result_payload,
    }


def _strip_gate_identity_privileged(event: dict) -> dict:
    """Privileged/audit view: preserves full signer identity surface."""
    if not isinstance(event, dict):
        event = {}
    payload = event.get("payload")
    if not isinstance(payload, dict):
        payload = {}
    node_id = str(event.get("node_id", "") or "")
    public_key = str(event.get("public_key", "") or "")
    public_key_algo = str(event.get("public_key_algo", "") or "")
    if node_id and not public_key:
        gate_id = str(payload.get("gate", "") or "")
        if gate_id:
            try:
                binding = _lookup_gate_member_binding(gate_id, node_id)
                if binding:
                    public_key, public_key_algo = binding
            except Exception:
                pass
    return {
        "event_id": str(event.get("event_id", "") or ""),
        "event_type": "gate_message",
        "timestamp": _redacted_gate_timestamp(event),
        "node_id": node_id,
        "sequence": int(event.get("sequence", 0) or 0),
        "signature": str(event.get("signature", "") or ""),
        "public_key": public_key,
        "public_key_algo": public_key_algo,
        "protocol_version": str(event.get("protocol_version", "") or ""),
        "payload": {
            "gate": str(payload.get("gate", "") or ""),
            "ciphertext": str(payload.get("ciphertext", "") or ""),
            "format": str(payload.get("format", "") or ""),
            "nonce": str(payload.get("nonce", "") or ""),
            "sender_ref": str(payload.get("sender_ref", "") or ""),
            "transport_lock": str(payload.get("transport_lock", "") or ""),
            "gate_envelope": str(payload.get("gate_envelope", "") or ""),
            "envelope_hash": str(payload.get("envelope_hash", "") or ""),
            "reply_to": _trusted_gate_reply_to(event),
        },
    }


def _strip_gate_identity(event: dict) -> dict:
    """Legacy alias ├óΓé¼ΓÇ¥ defaults to member (narrowed) view."""
    return _strip_gate_identity_member(event)


def _resolve_envelope_policy(gate_id: str) -> str:
    """Look up envelope_policy for a gate.

    Per-gate policy is the source of truth. The global recovery-envelope
    runtime switches are retained for legacy config/reporting, but consulting
    them here silently downgrades envelope_always rooms into unreadable
    member views.
    """
    try:
        from services.mesh.mesh_reputation import gate_manager

        return str(gate_manager.get_envelope_policy(gate_id) or "envelope_disabled")
    except Exception:
        return "envelope_disabled"


def _strip_gate_for_access(event: dict, access: str) -> dict:
    """Select member or privileged strip based on access level."""
    if access == "privileged":
        return _strip_gate_identity_privileged(event)
    payload = event.get("payload") if isinstance(event, dict) else None
    gate_id = str((payload or {}).get("gate", "") or "")
    envelope_policy = _resolve_envelope_policy(gate_id) if gate_id else "envelope_disabled"
    return _strip_gate_identity_member(event, envelope_policy=envelope_policy)


def _lookup_gate_member_binding(gate_id: str, node_id: str) -> tuple[str, str] | None:
    gate_key = str(gate_id or "").strip().lower()
    candidate = str(node_id or "").strip()
    if not gate_key or not candidate:
        return None
    try:
        from services.mesh.mesh_wormhole_persona import (
            bootstrap_wormhole_persona_state,
            read_wormhole_persona_state,
        )

        bootstrap_wormhole_persona_state()
        state = read_wormhole_persona_state()
    except Exception:
        return None
    for persona in list(state.get("gate_personas", {}).get(gate_key) or []):
        if str(persona.get("node_id", "") or "").strip() != candidate:
            continue
        public_key = str(persona.get("public_key", "") or "").strip()
        public_key_algo = str(persona.get("public_key_algo", "Ed25519") or "Ed25519").strip()
        if public_key and public_key_algo:
            return public_key, public_key_algo
    session = dict(state.get("gate_sessions", {}).get(gate_key) or {})
    if str(session.get("node_id", "") or "").strip() == candidate:
        public_key = str(session.get("public_key", "") or "").strip()
        public_key_algo = str(session.get("public_key_algo", "Ed25519") or "Ed25519").strip()
        if public_key and public_key_algo:
            return public_key, public_key_algo
    return None


def _resolve_gate_proof_identity(gate_id: str) -> dict[str, Any] | None:
    from services.mesh.mesh_wormhole_persona import (
        bootstrap_wormhole_persona_state,
        enter_gate_anonymously,
        read_wormhole_persona_state,
    )

    gate_key = str(gate_id or "").strip().lower()
    if not gate_key:
        return None
    bootstrap_wormhole_persona_state()
    state = read_wormhole_persona_state()
    session_identity = dict(state.get("gate_sessions", {}).get(gate_key) or {})
    if session_identity.get("private_key"):
        return session_identity
    active_persona_id = str(state.get("active_gate_personas", {}).get(gate_key, "") or "")
    for persona in list(state.get("gate_personas", {}).get(gate_key) or []):
        if str(persona.get("persona_id", "") or "") == active_persona_id:
            return dict(persona or {})
    for persona in list(state.get("gate_personas", {}).get(gate_key) or []):
        if persona.get("private_key"):
            return dict(persona or {})
    entered = enter_gate_anonymously(gate_key, rotate=False)
    if not entered.get("ok"):
        return None
    state = read_wormhole_persona_state()
    session_identity = dict(state.get("gate_sessions", {}).get(gate_key) or {})
    if session_identity.get("private_key"):
        return session_identity
    return None


def _sign_gate_access_proof(gate_id: str) -> dict[str, Any]:
    gate_key = str(gate_id or "").strip().lower()
    if not gate_key:
        return {"ok": False, "detail": "gate_id required"}
    identity = _resolve_gate_proof_identity(gate_key)
    if not identity:
        return {"ok": False, "detail": "gate_access_proof_unavailable"}
    private_key_b64 = str(identity.get("private_key", "") or "").strip()
    node_id = str(identity.get("node_id", "") or "").strip()
    public_key = str(identity.get("public_key", "") or "").strip()
    public_key_algo = str(identity.get("public_key_algo", "Ed25519") or "Ed25519").strip()
    if not (private_key_b64 and node_id and public_key and public_key_algo):
        return {"ok": False, "detail": "gate_access_proof_unavailable"}
    try:
        from cryptography.hazmat.primitives.asymmetric import ec, ed25519

        ts = int(time.time())
        challenge = f"{gate_key}:{ts}"
        key_bytes = base64.b64decode(private_key_b64)
        algo = parse_public_key_algo(public_key_algo)
        if algo == "Ed25519":
            signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
            signature = signing_key.sign(challenge.encode("utf-8"))
        elif algo == "ECDSA_P256":
            from cryptography.hazmat.primitives import hashes

            signing_key = ec.derive_private_key(int.from_bytes(key_bytes, "big"), ec.SECP256R1())
            signature = signing_key.sign(challenge.encode("utf-8"), ec.ECDSA(hashes.SHA256()))
        else:
            return {"ok": False, "detail": "gate_access_proof_unsupported_algo"}
    except Exception as exc:
        logger.warning("Gate access proof signing failed: %s", type(exc).__name__)
        return {"ok": False, "detail": "gate_access_proof_failed"}
    return {
        "ok": True,
        "gate_id": gate_key,
        "node_id": node_id,
        "ts": ts,
        "proof": base64.b64encode(signature).decode("ascii"),
    }


def _verify_gate_access(request: Request, gate_id: str) -> str:
    """Verify gate access. Returns 'privileged', 'member', or '' (denied)."""
    ok, _detail, _scope_class = _check_explicit_scoped_auth_local(
        request,
        {"gate.audit", "mesh.audit"},
    )
    if ok:
        return "privileged"
    ok, _detail = _check_scoped_auth(request, "gate")
    if ok:
        return "member"

    gate_key = str(gate_id or "").strip().lower()
    node_id = str(request.headers.get("x-wormhole-node-id", "") or "").strip()
    proof_b64 = str(request.headers.get("x-wormhole-gate-proof", "") or "").strip()
    ts_str = str(request.headers.get("x-wormhole-gate-ts", "") or "").strip()
    if not gate_key or not node_id or not proof_b64 or not ts_str:
        return ""
    try:
        ts = int(ts_str)
    except (TypeError, ValueError):
        return ""
    if abs(int(time.time()) - ts) > 60:
        return ""
    binding = _lookup_gate_member_binding(gate_key, node_id)
    if not binding:
        return ""
    public_key, public_key_algo = binding
    if not verify_node_binding(node_id, public_key):
        return ""
    try:
        signature_hex = base64.b64decode(proof_b64, validate=True).hex()
    except Exception:
        return ""
    challenge = f"{gate_key}:{ts_str}"
    challenge_ok, _challenge_reason = verify_node_bound_signature(
        node_id=node_id,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature_hex=signature_hex,
        payload=challenge,
    )
    if challenge_ok:
        return "member"
    return ""


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Non-hostile transport auto-upgrade ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
#
# The mesh/wormhole middleware can try to bring the wormhole supervisor
# up in the background when a user hits a tier-gated route on a weak
# transport. This is a best-effort, short-deadline attempt so we never
# add significant latency to ordinary requests, and it is rate-limited
# by a cooldown so back-to-back failures do not thrash the supervisor.
_TRANSPORT_UPGRADE_COOLDOWN_S = 30.0
_TRANSPORT_UPGRADE_DEADLINE_S = 2.5
_last_middleware_upgrade_attempt: float = 0.0
_middleware_upgrade_lock = asyncio.Lock()


async def _try_transparent_transport_upgrade() -> str | None:
    """Fire-and-wait-briefly attempt to upgrade the wormhole transport.

    Returns the current transport tier after the attempt (or after a
    cooldown skip), or None if the supervisor could not be probed.
    """
    global _last_middleware_upgrade_attempt

    async with _middleware_upgrade_lock:
        now = time.time()
        if (now - _last_middleware_upgrade_attempt) < _TRANSPORT_UPGRADE_COOLDOWN_S:
            try:
                from services.wormhole_supervisor import get_wormhole_state

                return _current_private_lane_tier(get_wormhole_state())
            except Exception:
                return None
        _last_middleware_upgrade_attempt = now

    def _blocking_upgrade() -> str | None:
        try:
            from services.wormhole_supervisor import (
                connect_wormhole,
                get_wormhole_state,
            )

            connect_wormhole(reason="middleware_auto_upgrade")
            return _current_private_lane_tier(get_wormhole_state())
        except Exception:
            return None

    try:
        return await asyncio.wait_for(
            asyncio.to_thread(_blocking_upgrade),
            timeout=_TRANSPORT_UPGRADE_DEADLINE_S,
        )
    except asyncio.TimeoutError:
        try:
            from services.wormhole_supervisor import get_wormhole_state

            return _current_private_lane_tier(get_wormhole_state())
        except Exception:
            return None


def _kickoff_dm_send_transport_upgrade() -> None:
    private_transport_manager.request_warmup(reason="queued_dm_delivery")


def _kickoff_private_control_transport_upgrade() -> None:
    private_transport_manager.request_warmup(reason="dm_surface_open")


def _private_surface_warmup_request(path: str, method: str) -> tuple[str, str] | None:
    normalized_path = str(path or "").strip()
    if normalized_path.startswith("/api/wormhole/dm/invite"):
        return ("invite_bootstrap", "private_control_only")
    if normalized_path.startswith("/api/wormhole/dm/contact"):
        return ("invite_bootstrap", "private_control_only")
    if normalized_path in {"/api/mesh/dm/prekey-bundle", "/api/mesh/dm/register"}:
        return ("invite_bootstrap", "private_control_only")
    if (
        normalized_path.startswith("/api/wormhole/dm/")
        or normalized_path.startswith("/api/mesh/dm/")
    ):
        return ("dm_surface_open", "private_control_only")
    if (
        normalized_path.startswith("/api/wormhole/gate/")
        or normalized_path.startswith("/api/mesh/gate/")
    ):
        return ("gate_surface_open", "private_control_only")
    return None


def _request_private_surface_warmup(*, path: str, method: str, current_tier: str) -> None:
    request = _private_surface_warmup_request(path, method)
    if request is None:
        return
    reason, required_tier = request
    private_transport_manager.request_warmup(
        reason=reason,
        current_tier=current_tier,
        required_tier=required_tier,
    )


def _is_invite_scoped_prekey_bundle_lookup(request: Request, path: str) -> bool:
    if request.method.upper() != "GET":
        return False
    normalized_path = str(path or "").strip()
    if normalized_path not in {"/api/mesh/dm/prekey-bundle", "/api/mesh/dm/pubkey"}:
        return False
    try:
        lookup_token = str(request.query_params.get("lookup_token", "") or "").strip()
        agent_id = str(request.query_params.get("agent_id", "") or "").strip()
    except Exception:
        return False
    return bool(lookup_token) and not agent_id


def _resume_private_delivery_background_work(*, current_tier: str, reason: str) -> None:
    pending_items = private_delivery_outbox.pending_items()
    if not pending_items:
        return
    private_release_worker.ensure_started()
    private_release_worker.wake()
    required_tier = "public_degraded"
    for item in pending_items:
        required_tier = release_lane_required_tier(str(item.get("lane", "") or ""))
        if required_tier == "private_strong":
            break
    private_transport_manager.request_warmup(
        reason=reason,
        current_tier=current_tier,
        required_tier=required_tier,
    )


def _is_public_meshtastic_lane_path(path: str, method: str) -> bool:
    """Routes for the public Meshtastic MQTT lane.

    These are intentionally outside the Wormhole/Infonet private transport
    lifecycle. Polling public MeshChat must not wake or re-enable Wormhole.
    """
    normalized_path = str(path or "").strip()
    method_name = str(method or "").upper()
    if method_name == "POST" and normalized_path == "/api/mesh/meshtastic/send":
        return True
    if method_name == "GET" and normalized_path in {
        "/api/mesh/messages",
        "/api/mesh/channels",
    }:
        return True
    return False


def _upgrade_invite_scoped_contact_preferences_background() -> dict[str, Any]:
    try:
        from services.mesh.mesh_wormhole_contacts import upgrade_invite_scoped_contact_preferences

        upgraded = int(upgrade_invite_scoped_contact_preferences() or 0)
        return {"ok": True, "upgraded_contacts": upgraded}
    except Exception as exc:
        return {
            "ok": False,
            "upgraded_contacts": 0,
            "detail": str(exc) or type(exc).__name__,
        }


def _refresh_lookup_handle_rotation_background(*, reason: str) -> dict[str, Any]:
    try:
        result = maybe_rotate_prekey_lookup_handles()
    except Exception as exc:
        logger.warning("lookup handle rotation check failed during %s: %s", str(reason or "").strip(), exc)
        return {
            "ok": False,
            "rotated": False,
            "state": "lookup_handle_rotation_failed",
            "detail": str(exc) or "lookup handle rotation failed",
        }
    return dict(result or {})


@app.middleware("http")
async def enforce_high_privacy_mesh(request: Request, call_next):
    path = _request_scope_path(request)
    private_mesh_path = path.startswith("/api/mesh") and not _is_public_meshtastic_lane_path(
        path,
        request.method,
    )
    if private_mesh_path or path.startswith("/api/wormhole/gate/") or path.startswith("/api/wormhole/dm/"):
        request.state._private_lane_started_at = time.perf_counter()
        current_tier = "public_degraded"
        try:
            from services.wormhole_supervisor import get_wormhole_state

            wormhole = get_wormhole_state()
        except Exception:
            wormhole = {"configured": False, "ready": False, "rns_ready": False}
        current_tier = _current_private_lane_tier(wormhole)
        request.state._private_lane_current_tier = current_tier
        try:
            _request_private_surface_warmup(
                path=path,
                method=request.method,
                current_tier=current_tier,
            )
        except Exception:
            logger.debug("Private surface warm-up request failed", exc_info=True)
        required_tier = _minimum_transport_tier(path, request.method)
        if required_tier:
            from services.mesh.mesh_privacy_policy import runtime_route_enforcement_tier

            required_tier = runtime_route_enforcement_tier(
                path,
                request.method,
                static_tier=required_tier,
            )
        if required_tier:
            if not _transport_tier_is_sufficient(current_tier, required_tier):
                if request.method.upper() == "POST" and path == "/api/mesh/dm/send":
                    # Non-hostile DM send path: accept user intent even when
                    # the strongest private transport is still converging.
                    # If Wormhole is already up at a weaker private tier,
                    # let the route continue silently. If we're still fully
                    # public_degraded, kick off background bring-up and let
                    # the route deliver with an honest relay-state detail.
                    request.state._dm_send_transport_pending = current_tier == "public_degraded"
                    if current_tier == "public_degraded":
                        try:
                            _kickoff_dm_send_transport_upgrade()
                        except Exception:
                            logger.debug("DM send background transport kickoff failed", exc_info=True)
                    request.state._private_lane_current_tier = current_tier
                elif (
                    request.method.upper() == "POST"
                    and path.startswith("/api/mesh/gate/")
                    and path.endswith("/message")
                ):
                    # Gate messages are sealed local writes first. Let the
                    # handler append ciphertext to the local gate store and
                    # queue fan-out; the release worker enforces the
                    # PRIVATE / STRONG network floor before peer propagation.
                    request.state._gate_message_transport_pending = True
                    if current_tier == "public_degraded":
                        try:
                            _kickoff_dm_send_transport_upgrade()
                        except Exception:
                            logger.debug("gate message background transport kickoff failed", exc_info=True)
                    request.state._private_lane_current_tier = current_tier
                elif required_tier == "private_control_only" and path.startswith("/api/wormhole/"):
                    # Local wormhole control routes prepare state, compose
                    # encrypted payloads, or manage keys locally. They
                    # should not hard-fail just because the hidden
                    # transport has not finished coming up yet.
                    request.state._private_control_transport_pending = current_tier == "public_degraded"
                    request.state._private_lane_current_tier = current_tier
                elif _is_invite_scoped_prekey_bundle_lookup(request, path):
                    # A copied DM address carries a high-entropy invite lookup
                    # handle. Returning the public prekey bundle for that
                    # handle is the bootstrap step that lets first contact get
                    # saved; blocking it behind the full private lane creates a
                    # circular warm-up failure. Stable agent_id lookup still
                    # follows the normal transport-tier policy.
                    request.state._invite_prekey_lookup_transport_pending = (
                        current_tier == "public_degraded"
                    )
                    request.state._private_lane_current_tier = current_tier
                else:
                    # Tor-style: instead of failing, keep trying in the
                    # background and return an ok:True "preparing" response
                    # (202 Accepted) so the client shows a spinner rather
                    # than an approval dialog. The request itself is NOT
                    # forwarded to the handler ΓÇö the tier is too low for the
                    # route's required privacy ΓÇö but the client can poll and
                    # retry transparently once the lane warms up.
                    try:
                        upgraded = await _try_transparent_transport_upgrade()
                    except Exception:
                        upgraded = current_tier
                        logger.debug("transparent transport upgrade failed", exc_info=True)
                    if upgraded is not None and _transport_tier_is_sufficient(
                        upgraded, required_tier
                    ):
                        current_tier = upgraded
                    else:
                        try:
                            _kickoff_dm_send_transport_upgrade()
                        except Exception:
                            logger.debug("background warmup kickoff failed", exc_info=True)
                        payload = _transport_tier_precondition_payload(
                            required_tier, upgraded or current_tier
                        )
                        payload["ok"] = True
                        payload["pending"] = True
                        payload["status"] = "preparing_private_lane"
                        return await _private_plane_refusal_response(
                            request,
                            status_code=202,
                            payload=payload,
                        )
        try:
            from services.wormhole_settings import read_wormhole_settings, write_wormhole_settings

            data = read_wormhole_settings()
            # Tor-style: if the user selected high privacy but Wormhole
            # isn't enabled yet, just turn it on and kick off warmup.
            # Don't block the request on the upgrade ΓÇö the transport
            # manager will converge in the background.
            if (
                private_mesh_path
                and str(data.get("privacy_profile", "default")).lower() == "high"
                and not bool(data.get("enabled"))
            ):
                try:
                    write_wormhole_settings(enabled=True)
                except Exception:
                    logger.debug("auto-enable wormhole (high privacy) failed", exc_info=True)
                try:
                    _kickoff_dm_send_transport_upgrade()
                except Exception:
                    logger.debug("high-privacy warmup kickoff failed", exc_info=True)
        except Exception:
            pass
        state = _anonymous_mode_state()
        if state["enabled"] and (
            _is_anonymous_mesh_write_path(path, request.method)
            or _is_anonymous_dm_action_path(path, request.method)
            or _is_anonymous_wormhole_gate_admin_path(path, request.method)
        ):
            # Tor-style: anonymous mode is on ΓåÆ do whatever is required for
            # it to function. Auto-enable Wormhole if off, and schedule
            # hidden-transport warmup WITHOUT blocking this request. The
            # transport manager converges in the background; the user sees
            # a normal (non-428) response in the meantime.
            if not state["wormhole_enabled"]:
                try:
                    from services.wormhole_settings import write_wormhole_settings

                    write_wormhole_settings(enabled=True)
                except Exception:
                    logger.debug("auto-enable wormhole (anonymous mode) failed", exc_info=True)
            if not state["ready"]:
                try:
                    _kickoff_dm_send_transport_upgrade()
                except Exception:
                    logger.debug("anonymous-mode warmup kickoff failed", exc_info=True)
    return await call_next(request)


@app.middleware("http")
async def apply_no_store_to_sensitive_paths(request: Request, call_next):
    response = await call_next(request)
    if _is_sensitive_no_store_path(_request_scope_path(request)):
        for key, value in _NO_STORE_HEADERS.items():
            response.headers[key] = value
    return response

# ---------------------------------------------------------------------------
# Register routers
# ---------------------------------------------------------------------------
app.include_router(health_router)
app.include_router(cctv_router)
app.include_router(radio_router)
app.include_router(sigint_router)
app.include_router(tools_router)
app.include_router(admin_router)
app.include_router(data_router)
app.include_router(mesh_peer_sync_router)
app.include_router(mesh_operator_router)
app.include_router(mesh_oracle_router)
app.include_router(mesh_dm_router)
app.include_router(mesh_public_router)
app.include_router(wormhole_router)
app.include_router(ai_intel_router)
app.include_router(sar_router)
app.include_router(infonet_router)
app.include_router(road_corridors_router)
app.include_router(osint_router)
app.include_router(scm_router)
app.include_router(entity_graph_router)
app.include_router(intel_feeds_router)
app.include_router(analytics_router)
app.include_router(agent_shell_router)

from services.data_fetcher import update_all_data

_refresh_lock = threading.Lock()


@app.get("/api/refresh", response_model=RefreshResponse, dependencies=[Depends(require_admin)])
@limiter.limit("2/minute")
async def force_refresh(request: Request):
    if not _refresh_lock.acquire(blocking=False):
        return {"status": "refresh already in progress"}

    def _do_refresh():
        try:
            update_all_data()
        finally:
            _refresh_lock.release()

    t = threading.Thread(target=_do_refresh)
    t.start()
    return {"status": "refreshing in background"}


@app.post("/api/ais/feed", dependencies=[Depends(require_local_operator)])
@limiter.limit("60/minute")
async def ais_feed(request: Request):
    """Accept AIS-catcher HTTP JSON feed (POST decoded AIS messages)."""
    from services.ais_stream import ingest_ais_catcher

    try:
        body = await request.json()
    except Exception:
        return JSONResponse(status_code=422, content={"ok": False, "detail": "invalid JSON body"})

    msgs = body.get("msgs", [])
    if not msgs:
        return {"status": "ok", "ingested": 0}

    count = ingest_ais_catcher(msgs)
    return {"status": "ok", "ingested": count}


from pydantic import BaseModel


class ViewportUpdate(BaseModel):
    s: float
    w: float
    n: float
    e: float


_LAST_VIEWPORT_UPDATE: tuple[float, float, float, float] | None = None
_LAST_VIEWPORT_UPDATE_TS = 0.0
_VIEWPORT_UPDATE_LOCK = threading.Lock()
_VIEWPORT_DEDUPE_EPSILON = 1.0
_VIEWPORT_MIN_UPDATE_S = 10.0


def _normalize_longitude(value: float) -> float:
    normalized = ((value + 180.0) % 360.0 + 360.0) % 360.0 - 180.0
    if normalized == -180.0 and value > 0:
        return 180.0
    return normalized


def _normalize_viewport_bounds(s: float, w: float, n: float, e: float) -> tuple[float, float, float, float]:
    south = max(-90.0, min(90.0, s))
    north = max(-90.0, min(90.0, n))
    raw_width = abs(e - w)
    if not math.isfinite(raw_width) or raw_width >= 360.0:
        return south, -180.0, north, 180.0
    west = _normalize_longitude(w)
    east = _normalize_longitude(e)
    if east < west:
        return south, -180.0, north, 180.0
    return south, west, north, east


def _viewport_changed_enough(bounds: tuple[float, float, float, float]) -> bool:
    global _LAST_VIEWPORT_UPDATE, _LAST_VIEWPORT_UPDATE_TS
    now = time.monotonic()
    with _VIEWPORT_UPDATE_LOCK:
        if _LAST_VIEWPORT_UPDATE is None:
            _LAST_VIEWPORT_UPDATE = bounds
            _LAST_VIEWPORT_UPDATE_TS = now
            return True
        changed = any(
            abs(current - previous) > _VIEWPORT_DEDUPE_EPSILON
            for current, previous in zip(bounds, _LAST_VIEWPORT_UPDATE)
        )
        if not changed and (now - _LAST_VIEWPORT_UPDATE_TS) < _VIEWPORT_MIN_UPDATE_S:
            return False
        if (now - _LAST_VIEWPORT_UPDATE_TS) < _VIEWPORT_MIN_UPDATE_S:
            return False
        _LAST_VIEWPORT_UPDATE = bounds
        _LAST_VIEWPORT_UPDATE_TS = now
        return True


def _queue_viirs_change_refresh() -> None:
    from services.fetchers.earth_observation import fetch_viirs_change_nodes

    threading.Thread(target=fetch_viirs_change_nodes, daemon=True).start()


@app.post("/api/viewport")
@limiter.limit("60/minute")
async def update_viewport(vp: ViewportUpdate, request: Request):  # noqa: ARG001
    """Receive frontend map bounds. AIS stream stays global so open-ocean
    vessels are never dropped ├óΓé¼ΓÇ¥ the frontend worker handles viewport culling."""
    return {"status": "ok"}


class LayerUpdate(BaseModel):
    layers: dict[str, bool]


@app.post("/api/layers", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def update_layers(update: LayerUpdate, request: Request):
    """Receive frontend layer toggle state. Starts/stops streams accordingly."""
    from services.fetchers._store import active_layers, bump_active_layers_version, is_any_active
    from services.layer_enable_refresh import refresh_newly_enabled_layers, snapshot_active_layers

    layers_before = snapshot_active_layers()
    # Snapshot old stream states before applying changes
    old_ships = is_any_active(
        "ships_military", "ships_cargo", "ships_civilian", "ships_passenger", "ships_tracked_yachts"
    )
    old_mesh = is_any_active("sigint_meshtastic")
    old_aprs = is_any_active("sigint_aprs")
    old_viirs = is_any_active("viirs_nightlights")

    # Update only known keys
    changed = False
    for key, value in update.layers.items():
        if key in active_layers:
            if active_layers[key] != value:
                changed = True
            active_layers[key] = value

    if changed:
        bump_active_layers_version()

    new_ships = is_any_active(
        "ships_military", "ships_cargo", "ships_civilian", "ships_passenger", "ships_tracked_yachts"
    )
    new_mesh = is_any_active("sigint_meshtastic")
    new_aprs = is_any_active("sigint_aprs")
    new_viirs = is_any_active("viirs_nightlights")

    # Start/stop AIS stream on transition
    if old_ships and not new_ships:
        from services.ais_stream import stop_ais_stream

        stop_ais_stream()
        logger.info("AIS stream stopped (all ship layers disabled)")
    elif not old_ships and new_ships:
        from services.ais_stream import start_ais_stream

        start_ais_stream()
        logger.info("AIS stream started (ship layer enabled)")

    # Start/stop SIGINT bridges on transition
    from services.sigint_bridge import sigint_grid

    if old_mesh and not new_mesh:
        try:
            from services.meshtastic_mqtt_settings import mqtt_bridge_enabled
            keep_chat_running = mqtt_bridge_enabled()
        except Exception:
            keep_chat_running = False
        if keep_chat_running:
            logger.info("Meshtastic map layer disabled; MQTT bridge kept running for MeshChat")
        else:
            sigint_grid.mesh.stop()
            logger.info("Meshtastic MQTT bridge stopped (layer disabled)")
    elif not old_mesh and new_mesh:
        # Respect the global MESH_MQTT_ENABLED gate even when the UI layer is
        # toggled on. The layer toggle should not bypass the opt-in flag that
        # protects the public broker from passive connection load.
        try:
            mqtt_enabled = bool(getattr(get_settings(), "MESH_MQTT_ENABLED", False))
        except Exception:
            mqtt_enabled = False
        if mqtt_enabled:
            sigint_grid.mesh.start()
            logger.info("Meshtastic MQTT bridge started (layer enabled)")
        else:
            logger.info(
                "Meshtastic layer enabled; MQTT bridge remains disabled "
                "(set MESH_MQTT_ENABLED=true to participate in the public broker)"
            )

    if old_aprs and not new_aprs:
        sigint_grid.aprs.stop()
        logger.info("APRS bridge stopped (layer disabled)")
    elif not old_aprs and new_aprs:
        sigint_grid.aprs.start()
        logger.info("APRS bridge started (layer enabled)")

    if not old_viirs and new_viirs:
        _queue_viirs_change_refresh()
        logger.info("VIIRS change refresh queued (layer enabled)")

    refresh_newly_enabled_layers(layers_before)

    return {"status": "ok"}


@app.get("/api/oracle/region-intel")
@limiter.limit("30/minute")
async def oracle_region_intel(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
):
    """Get oracle intelligence summary for a geographic region."""
    from services.oracle_service import get_region_oracle_intel

    news_items = get_latest_data().get("news", [])
    return get_region_oracle_intel(lat, lng, news_items)


@app.get("/api/thermal/verify")
@limiter.limit("10/minute")
async def thermal_verify(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
    radius_km: float = Query(10, ge=1, le=100),
):
    """On-demand thermal anomaly verification using Sentinel-2 SWIR bands."""
    from services.thermal_sentinel import search_thermal_anomaly

    result = search_thermal_anomaly(lat, lng, radius_km)
    return result


@app.post("/api/sigint/transmit")
@limiter.limit("5/minute")
async def sigint_transmit(request: Request):
    """Send an APRS-IS message to a specific callsign. Requires ham radio credentials."""
    from services.wormhole_supervisor import get_transport_tier

    tier = get_transport_tier()
    if str(tier or "").startswith("private_"):
        return {"ok": False, "detail": "APRS transmit blocked in private transport mode"}
    body = await request.json()
    callsign = body.get("callsign", "")
    passcode = body.get("passcode", "")
    target = body.get("target", "")
    message = body.get("message", "")
    if not all([callsign, passcode, target, message]):
        return {
            "ok": False,
            "detail": "Missing required fields: callsign, passcode, target, message",
        }
    from services.sigint_bridge import send_aprs_message

    return send_aprs_message(callsign, passcode, target, message)


@app.get("/api/sigint/nearest-sdr")
@limiter.limit("30/minute")
async def nearest_sdr(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
):
    """Find the nearest KiwiSDR receivers to a given coordinate."""
    from services.sigint_bridge import find_nearest_kiwisdr

    kiwisdr_data = get_latest_data().get("kiwisdr", [])
    return find_nearest_kiwisdr(lat, lng, kiwisdr_data)


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Per-Identity Throttle State ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
# In-memory: {node_id: {"last_send": timestamp, "daily_count": int, "daily_reset": timestamp}}
# Bounded to 10000 entries with 24hr TTL to prevent unbounded memory growth
_node_throttle: TTLCache = TTLCache(maxsize=10000, ttl=86400)
_gate_post_cooldown: TTLCache = TTLCache(maxsize=20000, ttl=86400)

# Byte limits per payload type
_BYTE_LIMITS = {"text": 200, "pin": 300, "emergency": 200, "command": 200}


def _check_throttle(
    node_id: str, priority_str: str, transport_lock: str = ""
) -> tuple[bool, str]:
    """Per-identity rate limiting based on node age and reputation.

    Tiers:
      New (rep < 3, age < 24h):       1 msg / 5 min,  10/day
      Established (rep >= 3 OR > 24h): 1 msg / 2 min,  50/day
      Trusted (rep >= 10):             1 msg / 30 sec, 200/day
      Emergency:                       no throttle

    Meshtastic public mesh is intentionally looser in testnet mode:
      Any public mesh sender:          2 msgs / min, tier caps unchanged
    """
    if priority_str == "emergency":
        return True, ""

    now = time.time()
    state = _node_throttle.get(node_id)
    if not state:
        _node_throttle[node_id] = {
            "last_send": 0,
            "daily_count": 0,
            "daily_reset": now,
            "first_seen": now,
        }
        state = _node_throttle[node_id]

    # Reset daily counter at midnight
    if now - state["daily_reset"] > 86400:
        state["daily_count"] = 0
        state["daily_reset"] = now

    # Determine tier (reputation integration will come with Feature 2)
    age_hours = (now - state.get("first_seen", now)) / 3600
    rep_score = 0
    try:
        from services.mesh.mesh_reputation import reputation_ledger

        rep_score = reputation_ledger.get_reputation(node_id).get("overall", 0)
        age_hours = max(age_hours, reputation_ledger.get_node_age_days(node_id) * 24)
    except Exception:
        rep_score = 0

    if rep_score >= 20 or age_hours >= 168:
        interval, daily_cap, tier = 30, 200, "trusted"
    elif rep_score >= 5 or age_hours >= 48:
        interval, daily_cap, tier = 120, 75, "established"
    else:
        interval, daily_cap, tier = 300, 15, "new"

    if str(transport_lock or "").lower() == "meshtastic":
        interval = min(interval, 30)

    # Check daily cap
    if state["daily_count"] >= daily_cap:
        return (
            False,
            f"Daily message limit reached ({daily_cap} messages for {tier} nodes). Resets in {int(86400 - (now - state['daily_reset']))}s.",
        )

    # Check interval
    elapsed = now - state["last_send"]
    if elapsed < interval:
        remaining = int(interval - elapsed)
        return False, f"Rate limit: 1 message per {interval}s for {tier} nodes. Wait {remaining}s."

    # Allowed
    state["last_send"] = now
    state["daily_count"] += 1
    return True, ""


def _check_gate_post_cooldown(sender_id: str, gate_id: str) -> tuple[bool, str]:
    """Check cooldown ├óΓé¼ΓÇ¥ does NOT record it.  Call _record_gate_post_cooldown() after success."""
    gate_key = str(gate_id or "").strip().lower()
    sender_key = str(sender_id or "").strip()
    if not gate_key or not sender_key:
        return True, ""
    now = time.time()
    cooldown_key = f"{sender_key}:{gate_key}"
    last_post = float(_gate_post_cooldown.get(cooldown_key, 0) or 0)
    if last_post > 0:
        elapsed = now - last_post
        if elapsed < 30:
            remaining = max(1, math.ceil(30 - elapsed))
            return False, f"Gate post cooldown: wait {remaining}s before posting again."
    return True, ""


def _record_gate_post_cooldown(sender_id: str, gate_id: str) -> None:
    """Stamp the cooldown AFTER a successful gate post."""
    gate_key = str(gate_id or "").strip().lower()
    sender_key = str(sender_id or "").strip()
    if gate_key and sender_key:
        _gate_post_cooldown[f"{sender_key}:{gate_key}"] = time.time()


def _verify_signed_event(
    *,
    event_type: str,
    node_id: str,
    sequence: int,
    public_key: str,
    public_key_algo: str,
    signature: str,
    payload: dict,
    protocol_version: str,
) -> tuple[bool, str]:
    return _shared_verify_signed_event(
        event_type=event_type,
        node_id=node_id,
        sequence=sequence,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        payload=payload,
        protocol_version=protocol_version,
    )


def _apply_legacy_dm_signature_compat(
    *,
    tier: str,
    delivery_class: str,
    payload_format: str,
    session_welcome: str,
    sender_seal: str,
    relay_salt_hex: str,
    sig_reason: str,
) -> dict[str, Any]:
    result = {
        "ok": True,
        "detail": "",
        "status_code": 0,
        "format": str(payload_format or "dm1").strip().lower() or "dm1",
        "session_welcome": str(session_welcome or "").strip(),
        "sender_seal": str(sender_seal or "").strip(),
        "relay_salt": str(relay_salt_hex or "").strip().lower(),
        "legacy_compat": False,
    }
    if sig_reason != "legacy_dm_signature_compat":
        return result

    logger.warning(
        "legacy dm signature compatibility path used; unsigned modern fields stripped before transport"
    )
    result["legacy_compat"] = True
    result["format"] = "dm1"
    result["session_welcome"] = ""
    result["sender_seal"] = ""
    result["relay_salt"] = ""

    if str(tier or "").startswith("private_") and result["format"] == "dm1":
        result["ok"] = False
        result["status_code"] = 403
        result["detail"] = "MLS session required in private transport mode - dm1 blocked on raw send path"
        return result

    if (
        str(tier or "").startswith("private_")
        and str(delivery_class or "").strip().lower() == "shared"
        and bool(get_settings().MESH_DM_REQUIRE_SENDER_SEAL_SHARED)
    ):
        result["ok"] = False
        result["detail"] = "sealed sender required for shared private DMs"
        return result

    return result


def _preflight_signed_event_integrity(
    *,
    event_type: str,
    node_id: str,
    sequence: int,
    public_key: str,
    public_key_algo: str,
    signature: str,
    protocol_version: str,
) -> tuple[bool, str]:
    return _shared_preflight_signed_event_integrity(
        event_type=event_type,
        node_id=node_id,
        sequence=sequence,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        protocol_version=protocol_version,
    )


def _verify_signed_write(
    *,
    event_type: str,
    node_id: str,
    sequence: int,
    public_key: str,
    public_key_algo: str,
    signature: str,
    payload: dict,
    protocol_version: str,
) -> tuple[bool, str]:
    return _shared_verify_signed_write(
        event_type=event_type,
        node_id=node_id,
        sequence=sequence,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        payload=payload,
        protocol_version=protocol_version,
    )


def _verify_gate_message_signed_write(
    *,
    node_id: str,
    sequence: int,
    public_key: str,
    public_key_algo: str,
    signature: str,
    payload: dict,
    reply_to: str,
    protocol_version: str,
) -> tuple[bool, str, str]:
    return _shared_verify_gate_message_signed_write(
        node_id=node_id,
        sequence=sequence,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        payload=payload,
        reply_to=reply_to,
        protocol_version=protocol_version,
    )


def _recover_verified_gate_reply_to(
    *,
    node_id: str,
    sequence: int,
    public_key: str,
    public_key_algo: str,
    signature: str,
    payload: dict,
    reply_to: str,
    protocol_version: str,
) -> str:
    return _shared_recover_verified_gate_reply_to(
        node_id=node_id,
        sequence=sequence,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        payload=payload,
        reply_to=reply_to,
        protocol_version=protocol_version,
    )


def _signed_body(request: Request) -> dict[str, Any]:
    prepared = get_prepared_signed_write(request)
    if prepared is None:
        return {}
    return dict(prepared.body)


def _prepared_signed_write(request: Request):
    return get_prepared_signed_write(request)


@app.post("/api/mesh/send")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.MESH_SEND)
async def mesh_send(request: Request):
    """Unified mesh message endpoint ├óΓé¼ΓÇ¥ auto-routes via optimal transport.

    Body: { destination, message, priority?, channel?, node_id?, credentials? }
    The router picks APRS, Meshtastic, or Internet based on gate logic.
    Enforces byte limits and per-identity rate limiting.
    """
    body = _signed_body(request)
    destination = body.get("destination", "")
    message = body.get("message", "")
    if not destination or not message:
        return {"ok": False, "detail": "Missing required fields: destination, message"}

    # ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Byte limit enforcement ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
    payload_bytes = len(message.encode("utf-8"))
    payload_type = body.get("payload_type", "text")
    max_bytes = _BYTE_LIMITS.get(payload_type, 200)
    if payload_bytes > max_bytes:
        return {
            "ok": False,
            "detail": f"Message too long ({payload_bytes} bytes). Maximum: {max_bytes} bytes for {payload_type} messages.",
        }

    # ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Signature verification & node registration ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
    node_id = body.get("node_id", body.get("sender_id", "anonymous"))
    public_key = body.get("public_key", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("signature", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")
    signed_payload = {
        "message": message,
        "destination": destination,
        "channel": body.get("channel", "LongFast"),
        "priority": body.get("priority", "normal").lower(),
        "ephemeral": bool(body.get("ephemeral", False)),
    }
    if body.get("transport_lock"):
        signed_payload["transport_lock"] = str(body.get("transport_lock"))
    # Register node in reputation ledger (auto-creates if new)
    if node_id != "anonymous":
        try:
            from services.mesh.mesh_reputation import reputation_ledger

            reputation_ledger.register_node(node_id, public_key, public_key_algo)
        except Exception:
            pass  # Non-critical ├óΓé¼ΓÇ¥ don't block sends if reputation module fails

    # ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Per-identity throttle ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
    priority_str = signed_payload["priority"]
    transport_lock = str(body.get("transport_lock", "") or "").lower()
    throttle_ok, throttle_reason = _check_throttle(node_id, priority_str, transport_lock)
    if not throttle_ok:
        return {"ok": False, "detail": throttle_reason}

    from services.mesh.mesh_router import (
        MeshEnvelope,
        MeshtasticTransport,
        Priority,
        TransportResult,
        mesh_router,
    )

    priority_map = {
        "emergency": Priority.EMERGENCY,
        "high": Priority.HIGH,
        "normal": Priority.NORMAL,
        "low": Priority.LOW,
    }
    priority = priority_map.get(priority_str, Priority.NORMAL)

    # ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ C-1 fix: compute trust_tier from Wormhole state ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
    from services.wormhole_supervisor import get_transport_tier

    computed_tier = get_transport_tier()

    envelope = MeshEnvelope(
        sender_id=node_id,
        destination=destination,
        channel=body.get("channel", "LongFast"),
        priority=priority,
        payload=message,
        ephemeral=body.get("ephemeral", False),
        trust_tier=computed_tier,
    )

    credentials = body.get("credentials", {})
    # ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ C-2 fix: enforce tier before transport_lock dispatch ├óΓÇ¥Γé¼├óΓÇ¥Γé¼
    private_tier = str(envelope.trust_tier or "").startswith("private_")
    if transport_lock == "meshtastic":
        if private_tier:
            results = [TransportResult(
                False, "meshtastic",
                "Private-tier content cannot be sent over Meshtastic"
            )]
        elif not mesh_router.meshtastic.can_reach(envelope):
            results = [TransportResult(False, "meshtastic", "Message exceeds Meshtastic payload limit")]
        else:
            cb_ok, cb_reason = mesh_router.breakers["meshtastic"].check_and_record(envelope.priority)
            if not cb_ok:
                results = [TransportResult(False, "meshtastic", cb_reason)]
            else:
                envelope.route_reason = (
                    "Transport locked to Meshtastic public path"
                    if MeshtasticTransport._parse_node_id(destination) is None
                    else "Transport locked to Meshtastic public node-targeted path"
                )
                result = mesh_router.meshtastic.send(envelope, credentials)
                if result.ok:
                    envelope.routed_via = mesh_router.meshtastic.NAME
                results = [result]
    elif transport_lock == "aprs":
        if private_tier:
            results = [TransportResult(
                False, "aprs",
                "Private-tier content cannot be sent over APRS"
            )]
        else:
            results = mesh_router.route(envelope, credentials)
    else:
        results = mesh_router.route(envelope, credentials)
    any_ok = any(r.ok for r in results)

    # ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Mirror to Meshtastic bridge feed ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
    # The MQTT broker won't echo our own publishes back to our subscriber, so
    # inject successfully-sent channel broadcasts into the bridge directly.
    # Node-targeted packets must not appear in the public channel feed.
    is_direct_destination = MeshtasticTransport._parse_node_id(destination) is not None
    if any_ok and envelope.routed_via == "meshtastic" and not is_direct_destination:
        try:
            from services.sigint_bridge import sigint_grid

            bridge = sigint_grid.mesh
            if bridge:
                from datetime import datetime

                append_text = getattr(bridge, "append_text_message", None)
                message_record = (
                    {
                        "from": MeshtasticTransport.mesh_address_for_sender(node_id),
                        "to": "broadcast",
                        "text": message,
                        "region": credentials.get("mesh_region", "US"),
                        "root": credentials.get("mesh_region", "US"),
                        "channel": body.get("channel", "LongFast"),
                        "timestamp": datetime.utcnow().isoformat() + "Z",
                    }
                )
                if callable(append_text):
                    append_text(message_record)
                else:
                    bridge.messages.appendleft(message_record)
        except Exception:
            pass  # Non-critical

    return {
        "ok": any_ok,
        "message_id": envelope.message_id,
        "event_id": "",
        "routed_via": envelope.routed_via,
        "route_reason": envelope.route_reason,
        "direct": is_direct_destination,
        "channel_echo": not is_direct_destination,
        "results": [r.to_dict() for r in results],
    }


@app.get("/api/mesh/log")
@limiter.limit("30/minute")
async def mesh_log(request: Request):
    """Get recent mesh message routing log (audit trail)."""
    from services.mesh.mesh_router import mesh_router

    mesh_router.prune_message_log()
    entries = list(mesh_router.message_log)
    ok, _detail = _check_scoped_auth(request, "mesh.audit")
    if ok:
        return {"log": entries}
    public_entries = [entry for entry in (_public_mesh_log_entry(item) for item in entries) if entry]
    return {"log": public_entries}


@app.get("/api/mesh/status")
@limiter.limit("30/minute")
async def mesh_status(request: Request):
    """Get mesh system status including circuit breaker state."""
    from services.env_check import get_security_posture_warnings
    from services.mesh.mesh_router import mesh_router
    from services.sigint_bridge import sigint_grid

    mesh_router.prune_message_log()
    entries = list(mesh_router.message_log)
    sigs = sigint_grid.get_all_signals()
    aprs = sum(1 for s in sigs if s.get("source") == "aprs")
    mesh = sum(1 for s in sigs if s.get("source") == "meshtastic")
    js8 = sum(1 for s in sigs if s.get("source") == "js8call")
    ok, _detail = _check_scoped_auth(request, "mesh.audit")
    authenticated = _scoped_view_authenticated(request, "mesh.audit")
    response = {
        "circuit_breakers": {
            name: breaker.get_status() for name, breaker in mesh_router.breakers.items()
        },
        "message_log_size": len(entries) if ok else _public_mesh_log_size(entries),
        "signal_counts": {
            "aprs": aprs,
            "meshtastic": mesh,
            "js8call": js8,
            "total": aprs + mesh + js8,
        },
    }
    if ok:
        response["public_message_log_size"] = _public_mesh_log_size(entries)
        response["private_log_retention_seconds"] = int(
            getattr(get_settings(), "MESH_PRIVATE_LOG_TTL_S", 900) or 0
        )
        response["security_warnings"] = get_security_posture_warnings(get_settings())

    return _redact_public_mesh_status(response, authenticated=authenticated)


@app.get("/api/mesh/signals")
@limiter.limit("30/minute")
async def mesh_signals(
    request: Request,
    source: str = "",
    region: str = "",
    root: str = "",
    limit: int = 50,
):
    """Get SIGINT signals with optional source/region/root filters."""
    from services.fetchers.sigint import build_sigint_snapshot

    sigs, _channel_stats, totals = build_sigint_snapshot()
    if source:
        sigs = [s for s in sigs if s.get("source") == source.lower()]
    if region:
        region_filter = region.upper()
        sigs = [
            s
            for s in sigs
            if s.get("region", "").upper() == region_filter
            or s.get("root", "").upper() == region_filter
        ]
    if root:
        root_filter = root.upper()
        sigs = [s for s in sigs if s.get("root", "").upper() == root_filter]
    return {
        "signals": sigs[: min(limit, 500)],
        "total": len(sigs),
        "source_totals": totals,
    }


@app.get("/api/mesh/messages")
@limiter.limit("30/minute")
async def mesh_messages(
    request: Request,
    region: str = "",
    root: str = "",
    channel: str = "",
    limit: int = 30,
    include_direct: bool = False,
):
    """Get recent Meshtastic text messages from the MQTT bridge."""
    from services.sigint_bridge import sigint_grid

    bridge = sigint_grid.mesh
    if not bridge:
        return []
    msgs = list(bridge.messages)
    if region:
        region_filter = region.upper()
        msgs = [
            m
            for m in msgs
            if m.get("region", "").upper() == region_filter
            or m.get("root", "").upper() == region_filter
        ]
    if root:
        root_filter = root.upper()
        msgs = [m for m in msgs if m.get("root", "").upper() == root_filter]
    if channel:
        msgs = [m for m in msgs if m.get("channel", "").lower() == channel.lower()]
    if not include_direct:
        msgs = [
            m
            for m in msgs
            if str(m.get("to") or "broadcast").strip().lower() in {"", "broadcast", "^all"}
        ]
    return msgs[: min(limit, 100)]


@app.get("/api/mesh/channels")
@limiter.limit("30/minute")
async def mesh_channels(request: Request):
    """Get Meshtastic channel population stats ├óΓé¼ΓÇ¥ nodes per region/channel."""
    stats = get_latest_data().get("mesh_channel_stats", {})
    return stats


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Reputation Endpoints ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼

# Cached root node_id ├óΓé¼ΓÇ¥ avoids 5 encrypted disk reads per vote.
_root_node_id_cache: dict[str, object] = {"value": None, "ts": 0.0}
_ROOT_NODE_ID_TTL = 30.0  # seconds


def _cached_root_node_id() -> str:
    import time as _time

    now = _time.time()
    if _root_node_id_cache["value"] is not None and (now - float(_root_node_id_cache["ts"])) < _ROOT_NODE_ID_TTL:
        return str(_root_node_id_cache["value"])
    try:
        from services.mesh.mesh_wormhole_persona import read_wormhole_persona_state

        ps = read_wormhole_persona_state()
        nid = str(ps.get("root_identity", {}).get("node_id", "") or "").strip()
        _root_node_id_cache["value"] = nid
        _root_node_id_cache["ts"] = now
        return nid
    except Exception:
        return ""


@app.post("/api/mesh/vote")
@limiter.limit("30/minute")
@requires_signed_write(kind=SignedWriteKind.MESH_VOTE)
async def mesh_vote(request: Request):
    """Cast a reputation vote on a node.

    Body: {voter_id, voter_pubkey?, voter_sig?, target_id, vote: 1|-1, gate?: string}
    """
    from services.mesh.mesh_reputation import reputation_ledger

    body = _signed_body(request)
    voter_id = body.get("voter_id", "")
    target_id = body.get("target_id", "")
    vote = body.get("vote", 0)
    gate = body.get("gate", "")
    public_key = body.get("voter_pubkey", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("voter_sig", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")

    if not voter_id or not target_id:
        return {"ok": False, "detail": "Missing voter_id or target_id"}
    if vote not in (1, -1):
        return {"ok": False, "detail": "Vote must be 1 or -1"}

    gate_ok, gate_detail = _validate_gate_vote_context(voter_id, gate)
    if not gate_ok:
        return {"ok": False, "detail": gate_detail}
    gate = gate_detail or ""

    vote_payload = {"target_id": target_id, "vote": vote, "gate": gate}

    # Resolve stable local operator ID for duplicate-vote prevention.
    # Personas generate unique keypairs, so voter_id alone is insufficient ├óΓé¼ΓÇ¥
    # use the root identity's node_id as a stable anchor so switching personas
    # doesn't let the same operator vote multiple times on the same post.
    stable_voter_id = voter_id
    try:
        root_nid = _cached_root_node_id()
        if root_nid:
            stable_voter_id = root_nid
    except Exception:
        pass

    # Register node if not known
    reputation_ledger.register_node(voter_id, public_key, public_key_algo)

    ok, reason, vote_weight = reputation_ledger.cast_vote(stable_voter_id, target_id, vote, gate)

    # Record on Infonet
    if ok:
        try:
            from services.mesh.mesh_hashchain import infonet

            normalized_payload = normalize_payload("vote", vote_payload)
            infonet.append(
                event_type="vote",
                node_id=voter_id,
                payload=normalized_payload,
                signature=signature,
                sequence=sequence,
                public_key=public_key,
                public_key_algo=public_key_algo,
                protocol_version=protocol_version,
            )
        except Exception:
            pass

    return {"ok": ok, "detail": reason, "weight": round(vote_weight, 2)}


@app.post("/api/mesh/report")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.MESH_REPORT)
async def mesh_report(request: Request):
    """Report abusive or fraudulent behavior (signed, public, non-anonymous)."""
    body = _signed_body(request)
    reporter_id = body.get("reporter_id", "")
    target_id = body.get("target_id", "")
    reason = body.get("reason", "")
    gate = body.get("gate", "")
    evidence = body.get("evidence", "")
    public_key = body.get("public_key", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("signature", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")

    if not reporter_id or not target_id or not reason:
        return {"ok": False, "detail": "Missing reporter_id, target_id, or reason"}

    report_payload = {"target_id": target_id, "reason": reason, "gate": gate, "evidence": evidence}

    try:
        from services.mesh.mesh_reputation import reputation_ledger

        reputation_ledger.register_node(reporter_id, public_key, public_key_algo)
    except Exception:
        pass

    try:
        from services.mesh.mesh_hashchain import infonet

        normalized_payload = normalize_payload("abuse_report", report_payload)
        infonet.append(
            event_type="abuse_report",
            node_id=reporter_id,
            payload=normalized_payload,
            signature=signature,
            sequence=sequence,
            public_key=public_key,
            public_key_algo=public_key_algo,
            protocol_version=protocol_version,
        )
    except Exception:
        logger.exception("failed to record abuse report on infonet")
        return {"ok": False, "detail": "report_record_failed"}

    return {"ok": True, "detail": "Report recorded"}


@app.get("/api/mesh/reputation")
@limiter.limit("60/minute")
async def mesh_reputation(request: Request, node_id: str = ""):
    """Get reputation for a single node.

    Public callers receive a summary-only view; authenticated audit callers may
    access the richer breakdown.
    """
    from services.mesh.mesh_reputation import reputation_ledger

    if not node_id:
        return {"ok": False, "detail": "Provide ?node_id=xxx"}
    return reputation_ledger.get_reputation_log(
        node_id,
        detailed=_scoped_view_authenticated(request, "mesh.audit"),
    )


@app.get("/api/mesh/reputation/batch")
@limiter.limit("60/minute")
async def mesh_reputation_batch(request: Request, node_id: list[str] = Query(default=[])):
    """Get overall public reputation for multiple public node IDs."""
    from services.mesh.mesh_reputation import reputation_ledger

    normalized: list[str] = []
    seen: set[str] = set()
    for raw in list(node_id or []):
        candidate = str(raw or "").strip()
        if not candidate or candidate in seen:
            continue
        seen.add(candidate)
        normalized.append(candidate)
        if len(normalized) >= 100:
            break
    if not normalized:
        return {"ok": False, "detail": "Provide at least one node_id", "reputations": {}}
    return {
        "ok": True,
        "reputations": {
            candidate: reputation_ledger.get_reputation(candidate).get("overall", 0) or 0
            for candidate in normalized
        },
    }


@app.get("/api/mesh/reputation/all", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def mesh_reputation_all(request: Request):
    """Get all known node reputations."""
    from services.mesh.mesh_reputation import reputation_ledger

    return {"reputations": reputation_ledger.get_all_reputations()}


@app.post("/api/mesh/identity/rotate")
@limiter.limit("5/minute")
@requires_signed_write(kind=SignedWriteKind.IDENTITY_ROTATE)
async def mesh_identity_rotate(request: Request):
    """Link a new node_id to an old one via dual-signature rotation."""
    body = _signed_body(request)
    old_node_id = body.get("old_node_id", "").strip()
    old_public_key = body.get("old_public_key", "").strip()
    old_public_key_algo = body.get("old_public_key_algo", "").strip()
    old_signature = body.get("old_signature", "").strip()
    new_node_id = body.get("new_node_id", "").strip()
    new_public_key = body.get("new_public_key", "").strip()
    new_public_key_algo = body.get("new_public_key_algo", "").strip()
    new_signature = body.get("new_signature", "").strip()
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "").strip()

    if not (
        old_node_id
        and old_public_key
        and old_public_key_algo
        and old_signature
        and new_node_id
        and new_public_key
        and new_public_key_algo
        and new_signature
        and timestamp
    ):
        return {"ok": False, "detail": "Missing rotation fields"}
    if old_node_id == new_node_id:
        return {"ok": False, "detail": "old_node_id must differ from new_node_id"}
    if abs(timestamp - int(time.time())) > 7 * 86400:
        return {"ok": False, "detail": "Rotation timestamp is too far from current time"}

    rotation_payload = {
        "old_node_id": old_node_id,
        "old_public_key": old_public_key,
        "old_public_key_algo": old_public_key_algo,
        "new_public_key": new_public_key,
        "new_public_key_algo": new_public_key_algo,
        "timestamp": timestamp,
        "old_signature": old_signature,
    }

    old_sig_ok, old_sig_reason = verify_key_rotation_claim_signature(
        old_node_id=old_node_id,
        old_public_key=old_public_key,
        old_public_key_algo=old_public_key_algo,
        old_signature=old_signature,
        new_public_key=new_public_key,
        new_public_key_algo=new_public_key_algo,
        timestamp=timestamp,
    )
    if not old_sig_ok:
        return {"ok": False, "detail": old_sig_reason}

    from services.mesh.mesh_reputation import reputation_ledger

    reputation_ledger.register_node(new_node_id, new_public_key, new_public_key_algo)
    ok, reason = reputation_ledger.link_identities(old_node_id, new_node_id)
    if not ok:
        return {"ok": False, "detail": reason}

    # Record on Infonet
    try:
        from services.mesh.mesh_hashchain import infonet

        normalized_payload = normalize_payload("key_rotate", rotation_payload)
        infonet.append(
            event_type="key_rotate",
            node_id=new_node_id,
            payload=normalized_payload,
            signature=new_signature,
            sequence=sequence,
            public_key=new_public_key,
            public_key_algo=new_public_key_algo,
            protocol_version=protocol_version,
        )
    except Exception:
        pass

    return {"ok": True, "detail": "Identity linked"}


@app.post("/api/mesh/identity/revoke")
@limiter.limit("5/minute")
@requires_signed_write(kind=SignedWriteKind.IDENTITY_REVOKE)
async def mesh_identity_revoke(request: Request):
    """Revoke a node's key with a grace window."""
    body = _signed_body(request)
    node_id = body.get("node_id", "").strip()
    public_key = body.get("public_key", "").strip()
    public_key_algo = body.get("public_key_algo", "").strip()
    signature = body.get("signature", "").strip()
    revoked_at = _safe_int(body.get("revoked_at", 0) or 0)
    grace_until = _safe_int(body.get("grace_until", 0) or 0)
    reason = body.get("reason", "").strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "").strip()

    if not (node_id and public_key and public_key_algo and signature and revoked_at and grace_until):
        return {"ok": False, "detail": "Missing revocation fields"}

    now = int(time.time())
    max_grace = 7 * 86400
    if grace_until < revoked_at:
        return {"ok": False, "detail": "grace_until must be >= revoked_at"}
    if grace_until - revoked_at > max_grace:
        return {"ok": False, "detail": "Grace window too large (max 7 days)"}
    if abs(revoked_at - now) > max_grace:
        return {"ok": False, "detail": "revoked_at is too far from current time"}

    payload = {
        "revoked_public_key": public_key,
        "revoked_public_key_algo": public_key_algo,
        "revoked_at": revoked_at,
        "grace_until": grace_until,
        "reason": reason,
    }

    if payload["revoked_public_key"] != public_key:
        return {"ok": False, "detail": "revoked_public_key must match public_key"}
    if payload["revoked_public_key_algo"] != public_key_algo:
        return {"ok": False, "detail": "revoked_public_key_algo must match public_key_algo"}

    try:
        from services.mesh.mesh_hashchain import infonet

        normalized_payload = normalize_payload("key_revoke", payload)
        infonet.append(
            event_type="key_revoke",
            node_id=node_id,
            payload=normalized_payload,
            signature=signature,
            sequence=sequence,
            public_key=public_key,
            public_key_algo=public_key_algo,
            protocol_version=protocol_version,
        )
    except Exception:
        logger.exception("failed to record key revocation on infonet")
        return {"ok": False, "detail": "revocation_record_failed"}

    return {"ok": True, "detail": "Identity revoked"}


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Gate Endpoints ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼


@app.post("/api/mesh/gate/create")
@limiter.limit("5/hour")
@requires_signed_write(kind=SignedWriteKind.GATE_CREATE)
async def gate_create(request: Request):
    """Create a new reputation-gated community.

    Body: {creator_id, creator_pubkey?, creator_sig?, gate_id, display_name, rules?: {min_overall_rep, min_gate_rep}}
    """
    from services.mesh.mesh_reputation import (
        ALLOW_DYNAMIC_GATES,
        reputation_ledger,
        gate_manager,
    )

    if not ALLOW_DYNAMIC_GATES:
        return {"ok": False, "detail": "Gate creation is disabled for the fixed private launch catalog"}

    body = _signed_body(request)
    creator_id = body.get("creator_id", "")
    gate_id = body.get("gate_id", "")
    display_name = body.get("display_name", gate_id)
    rules = body.get("rules", {})
    public_key = body.get("creator_pubkey", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("creator_sig", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")

    if not creator_id or not gate_id:
        return {"ok": False, "detail": "Missing creator_id or gate_id"}

    gate_payload = {"gate_id": gate_id, "display_name": display_name, "rules": rules}

    reputation_ledger.register_node(creator_id, public_key, public_key_algo)

    ok, reason = gate_manager.create_gate(
        creator_id,
        gate_id,
        display_name,
        min_overall_rep=rules.get("min_overall_rep", 0),
        min_gate_rep=rules.get("min_gate_rep"),
    )

    # Record on Infonet
    if ok:
        try:
            from services.mesh.mesh_hashchain import infonet

            normalized_payload = normalize_payload("gate_create", gate_payload)
            infonet.append(
                event_type="gate_create",
                node_id=creator_id,
                payload=normalized_payload,
                signature=signature,
                sequence=sequence,
                public_key=public_key,
                public_key_algo=public_key_algo,
                protocol_version=protocol_version,
            )
        except Exception:
            pass

    return {"ok": ok, "detail": reason}


@app.get("/api/mesh/gate/list")
@limiter.limit("30/minute")
async def gate_list(request: Request):
    """List all known gates (public catalog ├óΓé¼ΓÇ¥ secrets are never included)."""
    from services.mesh.mesh_reputation import gate_manager

    return {"gates": gate_manager.list_gates()}


@app.get("/api/mesh/gate/{gate_id}")
@limiter.limit("30/minute")
async def gate_detail(request: Request, gate_id: str):
    """Get gate details including ratification status."""
    from services.mesh.mesh_reputation import gate_manager

    gate = gate_manager.get_gate(gate_id)
    if not gate:
        return {"ok": False, "detail": f"Gate '{gate_id}' not found"}
    gate["ratification"] = gate_manager.get_ratification_status(gate_id)
    return gate


@app.post("/api/mesh/gate/{gate_id}/message")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.GATE_MESSAGE)
async def gate_message(request: Request, gate_id: str):
    """Post a message to a gate. Checks entry rules against sender's reputation.

    Body: {sender_id, ciphertext, nonce, sender_ref, signature?}
    """
    body = _signed_body(request)
    return _submit_gate_message_envelope(request, gate_id, body)


def _submit_gate_message_envelope(request: Request, gate_id: str, body: dict[str, Any]) -> dict[str, Any]:
    """Validate and record an encrypted gate envelope on the private plane."""
    from services.mesh.mesh_reputation import reputation_ledger, gate_manager
    prepared = _prepared_signed_write(request)
    sender_id = body.get("sender_id", "")
    epoch = _safe_int(body.get("epoch", 0) or 0)
    ciphertext = str(body.get("ciphertext", ""))
    nonce = str(body.get("nonce", body.get("iv", "")))
    sender_ref = str(body.get("sender_ref", ""))
    payload_format = str(body.get("format", "mls1") or "mls1")
    public_key = body.get("public_key", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("signature", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")

    if not sender_id:
        return {"ok": False, "detail": "Missing sender_id"}
    if "message" in body and str(body.get("message", "")).strip():
        return {
            "ok": False,
            "detail": "Plaintext gate messages are no longer accepted. Submit an encrypted gate envelope.",
        }

    gate_envelope = str(body.get("gate_envelope", "") or "").strip()
    envelope_hash = str(body.get("envelope_hash", "") or "").strip()
    transport_lock = str(body.get("transport_lock", "") or "").strip().lower()
    reply_to = str(body.get("reply_to", "") or "").strip()
    if not transport_lock:
        return {"ok": False, "detail": "transport_lock is required on content-private signed writes"}
    if transport_lock != "private_strong":
        return {"ok": False, "detail": "gate messages require private_strong transport_lock"}
    envelope_policy = _resolve_envelope_policy(gate_id)
    if envelope_policy == "envelope_always" and not gate_envelope:
        return {"ok": False, "detail": "gate_envelope_required"}
    if gate_envelope and not envelope_hash:
        return {"ok": False, "detail": "gate_envelope requires signed envelope_hash"}
    if envelope_hash:
        import hashlib as _hl

        if not gate_envelope:
            return {"ok": False, "detail": "gate_envelope required when envelope_hash is present"}
        if (
            len(envelope_hash) != 64
            or envelope_hash != envelope_hash.lower()
            or any(ch not in "0123456789abcdef" for ch in envelope_hash)
        ):
            return {"ok": False, "detail": "invalid envelope_hash"}
        try:
            actual_envelope_hash = _hl.sha256(gate_envelope.encode("ascii")).hexdigest()
        except UnicodeEncodeError:
            return {"ok": False, "detail": "invalid gate_envelope"}
        if actual_envelope_hash != envelope_hash:
            return {"ok": False, "detail": "gate_envelope does not match envelope_hash"}

    gate_payload_input = {
        "gate": gate_id,
        "ciphertext": ciphertext,
        "nonce": nonce,
        "sender_ref": sender_ref,
        "format": payload_format,
    }
    if epoch > 0:
        gate_payload_input["epoch"] = epoch
    if envelope_hash:
        gate_payload_input["envelope_hash"] = envelope_hash
    gate_payload_input["transport_lock"] = transport_lock
    gate_payload = normalize_payload("gate_message", gate_payload_input)
    # Validate BEFORE adding gate_envelope (which is not a normalized field).
    payload_ok, payload_reason = validate_event_payload("gate_message", gate_payload)
    if not payload_ok:
        return {"ok": False, "detail": payload_reason}
    # gate_envelope is not part of the signed payload ├óΓé¼ΓÇ¥ envelope_hash binds it.
    # reply_to is signed for new compose flows; if only the legacy no-reply_to
    # signature verifies, strip it rather than accepting unauthenticated
    # threading metadata.
    if gate_envelope:
        gate_payload["gate_envelope"] = gate_envelope
    if reply_to:
        gate_payload["reply_to"] = reply_to
    # Signature verification payload excludes epoch and gate_envelope.
    # envelope_hash is signed when present.
    signature_gate_payload = {
        "gate": gate_id,
        "ciphertext": ciphertext,
        "nonce": nonce,
        "sender_ref": sender_ref,
        "format": payload_format,
    }
    if envelope_hash:
        signature_gate_payload["envelope_hash"] = envelope_hash
    signature_gate_payload["transport_lock"] = transport_lock
    if epoch > 0:
        signature_gate_payload["epoch"] = epoch

    if prepared is not None and prepared.kind == SignedWriteKind.GATE_MESSAGE:
        sig_ok = True
        sig_reason = str(prepared.reason or "ok")
        verified_reply_to = str(prepared.verified_reply_to or reply_to)
    else:
        # Verify envelope binding: if envelope_hash is signed, the submitted
        # gate_envelope must match. Checked after signature so the hash itself
        # is already authenticated.
        sig_ok, sig_reason, verified_reply_to = _verify_gate_message_signed_write(
            node_id=sender_id,
            sequence=sequence,
            public_key=public_key,
            public_key_algo=public_key_algo,
            signature=signature,
            payload=signature_gate_payload,
            reply_to=reply_to,
            protocol_version=protocol_version,
        )
    if verified_reply_to != reply_to:
        gate_payload.pop("reply_to", None)
        reply_to = verified_reply_to
    if not sig_ok:
        return {"ok": False, "detail": sig_reason}

    if epoch > 0:
        try:
            from services.mesh.mesh_gate_mls import inspect_local_gate_state

            gate_state = inspect_local_gate_state(gate_id, expected_epoch=epoch)
        except Exception:
            gate_state = {"ok": False, "repair_state": "gate_state_stale", "detail": "gate epoch check unavailable"}
        if not bool(gate_state.get("ok", False)):
            return {
                "ok": False,
                "detail": str(gate_state.get("repair_state") or gate_state.get("detail") or "gate_state_stale"),
                "current_epoch": _safe_int(gate_state.get("current_epoch", 0) or 0),
                "expected_epoch": epoch,
            }

    # Do not synthesize durable envelopes after signature verification. A
    # gate_envelope is trusted only when the author signed its envelope_hash.

    reputation_ledger.register_node(sender_id, public_key, public_key_algo)

    # Check gate access
    can_enter, reason = gate_manager.can_enter(sender_id, gate_id)
    if not can_enter:
        return {"ok": False, "detail": f"Gate access denied: {reason}"}

    cooldown_ok, cooldown_reason = _check_gate_post_cooldown(sender_id, gate_id)
    if not cooldown_ok:
        return {"ok": False, "detail": cooldown_reason}

    gate_manager.record_message(gate_id)
    _record_gate_post_cooldown(sender_id, gate_id)
    logger.info("Encrypted gate message accepted on obfuscated gate plane")

    # Build and commit the encrypted gate event to the private Infonet ledger.
    # The main hashchain is the durable propagation surface; gate_store is the
    # local materialized view used by the existing decrypt/UI path.
    try:
        from services.mesh.mesh_hashchain import infonet
        import time as _time

        store_payload = dict(gate_payload)
        if sig_reason in {"legacy_gate_epoch_signature_compat", "legacy_gate_epoch_reply_signature_compat"}:
            store_payload.pop("epoch", None)
        if gate_envelope:
            store_payload["gate_envelope"] = gate_envelope
        if reply_to:
            store_payload["reply_to"] = reply_to

        gate_event = {
            "event_type": "gate_message",
            "node_id": sender_id,
            "payload": store_payload,
            "timestamp": _time.time(),
            "sequence": sequence,
            "signature": signature,
            "public_key": public_key,
            "public_key_algo": public_key_algo,
            "protocol_version": protocol_version or PROTOCOL_VERSION,
        }
        gate_event = infonet.append_private_gate_message(
            node_id=sender_id,
            payload=store_payload,
            signature=signature,
            sequence=sequence,
            public_key=public_key,
            public_key_algo=public_key_algo,
            protocol_version=protocol_version or PROTOCOL_VERSION,
            timestamp=float(gate_event.get("timestamp", 0) or 0),
        )
    except ValueError as exc:
        return {"ok": False, "detail": str(exc)}
    except Exception:
        logger.exception("Failed to append gate message to private Infonet ledger")
        return {"ok": False, "detail": "Failed to record gate message"}

    # Append to the local gate_store immediately so the author sees the same
    # materialized gate view that peers will hydrate after private sync.
    try:
        from services.mesh.mesh_hashchain import gate_store

        stored_event = gate_store.append(gate_id, gate_event)
        if isinstance(stored_event, dict) and stored_event.get("event_id"):
            gate_event["event_id"] = str(stored_event.get("event_id") or gate_event.get("event_id") or "")
    except Exception:
        logger.exception("Failed to persist gate message locally (gate_store.append)")
        return {"ok": False, "detail": "Failed to record gate message"}

    current_tier = str(
        getattr(request.state, "_private_lane_current_tier", "")
        or getattr(request.state, "_transport_tier", "")
        or "public_degraded"
    )
    return _queue_gate_release(
        current_tier=current_tier,
        gate_id=gate_id,
        payload={
            "gate_id": gate_id,
            "event_id": str(gate_event.get("event_id", "") or ""),
            "event": gate_event,
        },
    )


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Infonet Endpoints ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼


@app.get("/api/mesh/infonet/status")
@limiter.limit("30/minute")
async def infonet_status(request: Request, verify_signatures: bool = False):
    """Get Infonet metadata ├óΓé¼ΓÇ¥ event counts, head hash, chain size."""
    from services.mesh.mesh_hashchain import infonet
    from services.wormhole_supervisor import get_wormhole_state

    info = infonet.get_info()
    valid, reason = infonet.validate_chain(verify_signatures=verify_signatures)
    try:
        wormhole = get_wormhole_state()
    except Exception:
        wormhole = {"configured": False, "ready": False, "rns_ready": False}
    info["valid"] = valid
    info["validation"] = reason
    info["verify_signatures"] = verify_signatures
    info["private_lane_tier"] = _current_private_lane_tier(wormhole)
    info["private_lane_policy"] = _private_infonet_policy_snapshot(
        current_tier=info["private_lane_tier"]
    )
    info.update(_node_runtime_snapshot())
    return _redact_private_lane_control_fields(
        info,
        authenticated=_scoped_view_authenticated(request, "mesh.audit"),
    )


@app.get("/api/privacy/claims", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_privacy_claims(request: Request, exposure: str = "ordinary"):
    """Authoritative runtime privacy claims used by UI and release checks."""
    from services.wormhole_supervisor import get_wormhole_state

    try:
        wormhole = await asyncio.to_thread(get_wormhole_state)
    except Exception:
        wormhole = {"configured": False, "ready": False, "arti_ready": False, "rns_ready": False}
    current_tier = _current_private_lane_tier(wormhole)
    local_custody = local_custody_status_snapshot()
    privacy_core = _privacy_core_status()
    diagnostic_package = _diagnostic_review_package_snapshot(
        current_tier=current_tier,
        local_custody=local_custody,
        privacy_core=privacy_core,
    )
    result = {
        "ok": True,
        "authoritative_model": "privacy_claims",
        "transport_tier": current_tier,
        "privacy_claims": diagnostic_package.get("claim_surface", {}).get("privacy_claims"),
        "privacy_status": diagnostic_package.get("privacy_status"),
        "strong_claims": diagnostic_package.get("strong_claims"),
        "release_gate": diagnostic_package.get("release_gate"),
    }
    if str(exposure or "").strip().lower() == "diagnostic":
        result.update(
            {
                "rollout_readiness": diagnostic_package.get("rollout_readiness"),
                "rollout_controls": diagnostic_package.get("rollout_controls"),
                "rollout_health": diagnostic_package.get("rollout_health"),
                "claim_surface_sources": diagnostic_package.get("claim_surface_sources"),
                "review_export": diagnostic_package.get("review_export"),
            }
        )
    return result


@app.get("/api/mesh/infonet/merkle")
@limiter.limit("30/minute")
async def infonet_merkle(request: Request):
    """Merkle root for sync comparison."""
    from services.mesh.mesh_hashchain import infonet

    return {
        "merkle_root": infonet.get_merkle_root(),
        "head_hash": infonet.head_hash,
        "count": len(infonet.events),
        "network_id": infonet.get_info().get("network_id"),
    }


@app.get("/api/mesh/infonet/locator")
@limiter.limit("30/minute")
async def infonet_locator(request: Request, limit: int = Query(32, ge=4, le=128)):
    """Block locator for fork-aware sync."""
    from services.mesh.mesh_hashchain import infonet

    locator = infonet.get_locator(max_entries=limit)
    return {
        "locator": locator,
        "head_hash": infonet.head_hash,
        "count": len(infonet.events),
        "network_id": infonet.get_info().get("network_id"),
    }


@app.post("/api/mesh/infonet/sync")
@limiter.limit(_INFONET_SYNC_RATE_LIMIT)
@mesh_write_exempt(MeshWriteExemption.PEER_GOSSIP)
async def infonet_sync_post(
    request: Request,
    limit: int = Query(100, ge=1, le=500),
):
    """Fork-aware sync using a block locator."""
    from services.mesh.mesh_hashchain import infonet, GENESIS_HASH

    body = await request.json()
    req_proto = str(body.get("protocol_version", "") or "")
    if req_proto and req_proto != PROTOCOL_VERSION:
        return Response(
            content=json_mod.dumps(
                {
                    "ok": False,
                    "detail": "Unsupported protocol_version",
                    "protocol_version": PROTOCOL_VERSION,
                }
            ),
            status_code=426,
            media_type="application/json",
        )
    locator = body.get("locator", [])
    if not isinstance(locator, list):
        return {"ok": False, "detail": "locator must be a list"}
    expected_head = str(body.get("expected_head", "") or "")
    if expected_head and expected_head != infonet.head_hash:
        return Response(
            content=json_mod.dumps(
                {
                    "ok": False,
                    "detail": "head_hash mismatch",
                    "head_hash": infonet.head_hash,
                    "expected_head": expected_head,
                }
            ),
            status_code=409,
            media_type="application/json",
        )
    if "limit" in body:
        try:
            limit = max(1, min(500, _safe_int(body["limit"], 0)))
        except Exception:
            pass

    matched_hash, start_index, events = infonet.get_events_after_locator(locator, limit=limit)
    forked = False
    if not matched_hash:
        forked = True
    elif matched_hash == GENESIS_HASH and len(locator) > 1:
        forked = True

    events = _infonet_sync_response_events(events, request=request)

    response = {
        "events": events,
        "matched_hash": matched_hash,
        "forked": forked,
        "head_hash": infonet.head_hash,
        "count": len(events),
        "protocol_version": PROTOCOL_VERSION,
    }
    if body.get("include_proofs"):
        proofs = infonet.get_merkle_proofs(start_index, len(events)) if start_index >= 0 else {}
        response.update(
            {
                "merkle_root": proofs.get("root", infonet.get_merkle_root()),
                "merkle_total": proofs.get("total", len(infonet.events)),
                "merkle_start": proofs.get("start", 0),
                "merkle_proofs": proofs.get("proofs", []),
            }
        )
    return response


@app.get("/api/mesh/metrics")
@limiter.limit("30/minute")
async def mesh_metrics(request: Request):
    """Mesh protocol health counters."""
    from services.mesh.mesh_metrics import snapshot

    ok, detail = _check_scoped_auth(request, "mesh.audit")
    if not ok:
        if detail == "insufficient scope":
            raise HTTPException(status_code=403, detail="Forbidden ├óΓé¼ΓÇ¥ insufficient scope")
        raise HTTPException(status_code=403, detail=detail)
    return snapshot()


@app.get("/api/mesh/rns/status")
@limiter.limit("30/minute")
async def mesh_rns_status(request: Request):
    from services.wormhole_supervisor import get_wormhole_state

    try:
        from services.mesh.mesh_rns import rns_bridge

        status = await asyncio.to_thread(rns_bridge.status)
    except Exception:
        status = {"enabled": False, "ready": False, "configured_peers": 0, "active_peers": 0}
    try:
        wormhole = get_wormhole_state()
    except Exception:
        wormhole = {"configured": False, "ready": False, "rns_ready": False}
    status["private_lane_tier"] = _current_private_lane_tier(wormhole)
    status["private_lane_policy"] = _private_infonet_policy_snapshot(
        current_tier=status["private_lane_tier"]
    )
    return _redact_public_rns_status(
        status,
        authenticated=_scoped_view_authenticated(request, "mesh.audit"),
    )


@app.get("/api/mesh/infonet/sync")
@limiter.limit(_INFONET_SYNC_RATE_LIMIT)
async def infonet_sync(
    request: Request,
    after_hash: str = "",
    limit: int = Query(100, ge=1, le=500),
    expected_head: str = "",
    protocol_version: str = "",
):
    """Return events after a given hash (delta sync)."""
    from services.mesh.mesh_hashchain import infonet, GENESIS_HASH

    if protocol_version and protocol_version != PROTOCOL_VERSION:
        return Response(
            content=json_mod.dumps(
                {
                    "ok": False,
                    "detail": "Unsupported protocol_version",
                    "protocol_version": PROTOCOL_VERSION,
                }
            ),
            status_code=426,
            media_type="application/json",
        )
    if expected_head and expected_head != infonet.head_hash:
        return Response(
            content=json_mod.dumps(
                {
                    "ok": False,
                    "detail": "head_hash mismatch",
                    "head_hash": infonet.head_hash,
                    "expected_head": expected_head,
                }
            ),
            status_code=409,
            media_type="application/json",
        )
    base = after_hash or GENESIS_HASH
    events = infonet.get_events_after(base, limit=limit)
    events = _infonet_sync_response_events(events, request=request)
    return {
        "events": events,
        "after_hash": base,
        "count": len(events),
        "protocol_version": PROTOCOL_VERSION,
    }


@app.post("/api/mesh/infonet/ingest", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
async def infonet_ingest(request: Request):
    """Ingest externally sourced Infonet events (strict verification)."""
    from services.mesh.mesh_hashchain import infonet

    body = await request.json()
    events = body.get("events", [])
    expected_head = str(body.get("expected_head", "") or "")
    if expected_head and expected_head != infonet.head_hash:
        return Response(
            content=json_mod.dumps(
                {
                    "ok": False,
                    "detail": "head_hash mismatch",
                    "head_hash": infonet.head_hash,
                    "expected_head": expected_head,
                }
            ),
            status_code=409,
            media_type="application/json",
        )
    if not isinstance(events, list):
        return {"ok": False, "detail": "events must be a list"}
    if len(events) > 200:
        return {"ok": False, "detail": "Too many events in one ingest batch"}

    result = infonet.ingest_events(events)
    _hydrate_gate_store_from_chain(events)
    _hydrate_dm_relay_from_chain(events)
    return {"ok": True, **result}


@app.get("/api/mesh/infonet/peer-registry", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def infonet_peer_registry(request: Request):
    """Operator view of the live swarm peer registry (seed nodes only)."""
    from services.mesh.mesh_peer_registry import DEFAULT_PEER_REGISTRY_PATH, PeerRegistry
    from services.mesh.mesh_swarm_runtime import peer_registry_enabled

    if not peer_registry_enabled():
        return {"ok": False, "detail": "peer registry is not enabled on this node"}
    registry = PeerRegistry(DEFAULT_PEER_REGISTRY_PATH)
    try:
        peers = registry.load()
    except Exception as exc:
        return {"ok": False, "detail": str(exc or type(exc).__name__)}
    return {
        "ok": True,
        "peer_count": len(peers),
        "peers": [peer.to_dict() for peer in peers],
    }


@app.get("/api/mesh/infonet/bootstrap-manifest")
@limiter.limit(_INFONET_SYNC_RATE_LIMIT)
async def infonet_bootstrap_manifest(request: Request):
    """Return the current signed bootstrap/swarm peer manifest."""
    from services.mesh.mesh_swarm_runtime import load_live_bootstrap_manifest

    manifest = load_live_bootstrap_manifest()
    if manifest is None:
        return {"ok": False, "detail": "bootstrap manifest unavailable"}
    return {"ok": True, "manifest": manifest.to_dict()}


@app.post("/api/mesh/infonet/peer-announce")
@limiter.limit("30/minute")
@mesh_write_exempt(MeshWriteExemption.PEER_GOSSIP)
async def infonet_peer_announce(request: Request):
    """Register a participant onion peer in the swarm registry (HMAC-authenticated)."""
    from auth import _peer_hmac_url_from_request
    from services.mesh.mesh_swarm_runtime import peer_registry_enabled, record_peer_announcement

    body_bytes = await request.body()
    if not _verify_peer_transport_hmac(request, body_bytes):
        return Response(
            content='{"ok":false,"detail":"Invalid or missing peer HMAC"}',
            status_code=403,
            media_type="application/json",
        )
    if not peer_registry_enabled():
        return {"ok": False, "detail": "peer registry is not enabled on this node"}
    body = json_mod.loads(body_bytes or b"{}")
    announced_url = normalize_peer_url(str(body.get("peer_url", "") or ""))
    header_url = _peer_hmac_url_from_request(request)
    if not announced_url or announced_url != header_url:
        return {"ok": False, "detail": "peer_url must match X-Peer-Url"}
    peer = record_peer_announcement(body)
    return {"ok": True, "peer_url": peer.peer_url, "role": peer.role, "transport": peer.transport}


@app.post("/api/mesh/infonet/peer-push")
@limiter.limit("30/minute")
@mesh_write_exempt(MeshWriteExemption.PEER_GOSSIP)
async def infonet_peer_push(request: Request):
    """Accept pushed Infonet events from relay peers (HMAC-authenticated)."""
    content_length = request.headers.get("content-length")
    if content_length:
        try:
            if int(content_length) > 524_288:
                return Response(
                    content='{"ok":false,"detail":"Request body too large (max 512KB)"}',
                    status_code=413,
                    media_type="application/json",
                )
        except (ValueError, TypeError):
            pass
    from services.mesh.mesh_hashchain import infonet

    body_bytes = await request.body()
    if not _verify_peer_push_hmac(request, body_bytes):
        return Response(
            content='{"ok":false,"detail":"Invalid or missing peer HMAC"}',
            status_code=403,
            media_type="application/json",
        )

    body = json_mod.loads(body_bytes or b"{}")
    events = body.get("events", [])
    if not isinstance(events, list):
        return {"ok": False, "detail": "events must be a list"}
    if len(events) > 50:
        return {"ok": False, "detail": "Too many events in one push (max 50)"}
    if not events:
        return {"ok": True, "accepted": 0, "duplicates": 0, "rejected": []}

    result = infonet.ingest_events(events)
    _hydrate_gate_store_from_chain(events)
    _hydrate_dm_relay_from_chain(events)
    if any(str(event.get("event_type") or "") in {"gate_message", "dm_message"} for event in events):
        _kick_public_sync_background("peer_push_ingest")
    return {"ok": True, **result}


@app.post("/api/mesh/gate/peer-push")
@limiter.limit("30/minute")
@mesh_write_exempt(MeshWriteExemption.PEER_GOSSIP)
async def gate_peer_push(request: Request):
    """Accept pushed gate events from relay peers (private plane)."""
    content_length = request.headers.get("content-length")
    if content_length:
        try:
            if int(content_length) > 524_288:
                return Response(
                    content='{"ok":false,"detail":"Request body too large"}',
                    status_code=413,
                    media_type="application/json",
                )
        except (ValueError, TypeError):
            pass

    from services.mesh.mesh_hashchain import gate_store

    body_bytes = await request.body()
    if not _verify_peer_push_hmac(request, body_bytes):
        return Response(
            content='{"ok":false,"detail":"Invalid or missing peer HMAC"}',
            status_code=403,
            media_type="application/json",
        )

    body = json_mod.loads(body_bytes or b"{}")
    events = body.get("events", [])
    if not isinstance(events, list):
        return {"ok": False, "detail": "events must be a list"}
    if len(events) > 50:
        return {"ok": False, "detail": "Too many events (max 50)"}
    if not events:
        return {"ok": True, "accepted": 0, "duplicates": 0}

    from services.mesh.mesh_hashchain import resolve_gate_wire_ref

    grouped_events: dict[str, list[dict[str, Any]]] = {}
    for evt in events:
        evt_dict = evt if isinstance(evt, dict) else {}
        payload = evt_dict.get("payload")
        if not isinstance(payload, dict):
            payload = {}
        clean_event = {
            "event_id": str(evt_dict.get("event_id", "") or ""),
            "event_type": "gate_message",
            "timestamp": evt_dict.get("timestamp", 0),
            "node_id": str(evt_dict.get("node_id", "") or evt_dict.get("sender_id", "") or ""),
            "sequence": evt_dict.get("sequence", 0),
            "signature": str(evt_dict.get("signature", "") or ""),
            "public_key": str(evt_dict.get("public_key", "") or ""),
            "public_key_algo": str(evt_dict.get("public_key_algo", "") or ""),
            "protocol_version": str(evt_dict.get("protocol_version", "") or ""),
            "payload": {
                "ciphertext": str(payload.get("ciphertext", "") or ""),
                "format": str(payload.get("format", "") or ""),
                "nonce": str(payload.get("nonce", "") or ""),
                "sender_ref": str(payload.get("sender_ref", "") or ""),
            },
        }
        epoch = _safe_int(payload.get("epoch", 0) or 0)
        if epoch > 0:
            clean_event["payload"]["epoch"] = epoch
        # Preserve envelope metadata required for cross-node decryption and
        # authenticated threading.
        envelope_hash_val = str(payload.get("envelope_hash", "") or "").strip()
        gate_envelope_val = str(payload.get("gate_envelope", "") or "").strip()
        reply_to_val = str(payload.get("reply_to", "") or "").strip()
        if envelope_hash_val:
            clean_event["payload"]["envelope_hash"] = envelope_hash_val
        if gate_envelope_val:
            clean_event["payload"]["gate_envelope"] = gate_envelope_val
        transport_lock_val = str(payload.get("transport_lock", "") or "").strip().lower()
        if transport_lock_val:
            clean_event["payload"]["transport_lock"] = transport_lock_val
        if reply_to_val:
            clean_event["payload"]["reply_to"] = reply_to_val
        event_gate_id = str(payload.get("gate", "") or evt_dict.get("gate", "") or "").strip().lower()
        if not event_gate_id:
            event_gate_id = resolve_gate_wire_ref(
                str(payload.get("gate_ref", "") or evt_dict.get("gate_ref", "") or ""),
                clean_event,
            )
        if not event_gate_id:
            return {"ok": False, "detail": "gate resolution failed"}
        final_payload: dict[str, Any] = {
            "gate": event_gate_id,
            "ciphertext": clean_event["payload"]["ciphertext"],
            "format": clean_event["payload"]["format"],
            "nonce": clean_event["payload"]["nonce"],
            "sender_ref": clean_event["payload"]["sender_ref"],
        }
        if epoch > 0:
            final_payload["epoch"] = epoch
        if clean_event["payload"].get("envelope_hash"):
            final_payload["envelope_hash"] = clean_event["payload"]["envelope_hash"]
        if clean_event["payload"].get("gate_envelope"):
            final_payload["gate_envelope"] = clean_event["payload"]["gate_envelope"]
        if clean_event["payload"].get("transport_lock"):
            final_payload["transport_lock"] = clean_event["payload"]["transport_lock"]
        if clean_event["payload"].get("reply_to"):
            final_payload["reply_to"] = clean_event["payload"]["reply_to"]
        grouped_events.setdefault(event_gate_id, []).append(
            {
                "event_id": clean_event["event_id"],
                "event_type": "gate_message",
                "timestamp": clean_event["timestamp"],
                "node_id": clean_event["node_id"],
                "sequence": clean_event["sequence"],
                "signature": clean_event["signature"],
                "public_key": clean_event["public_key"],
                "public_key_algo": clean_event["public_key_algo"],
                "protocol_version": clean_event["protocol_version"],
                "payload": final_payload,
            }
        )

    accepted = 0
    duplicates = 0
    rejected = 0
    for event_gate_id, items in grouped_events.items():
        result = gate_store.ingest_peer_events(event_gate_id, items)
        a = int(result.get("accepted", 0) or 0)
        accepted += a
        duplicates += int(result.get("duplicates", 0) or 0)
        rejected += int(result.get("rejected", 0) or 0)
    return {"ok": True, "accepted": accepted, "duplicates": duplicates, "rejected": rejected}


@app.post("/api/mesh/gate/peer-pull")
@limiter.limit("30/minute")
@mesh_write_exempt(MeshWriteExemption.PEER_GOSSIP)
async def gate_peer_pull(request: Request):
    """Return gate events a peer is missing (HMAC-authenticated pull sync).

    Body: {"gate_id": "...", "after_count": N}
    Returns up to 50 events after the caller's known count for that gate.
    """
    content_length = request.headers.get("content-length")
    if content_length:
        try:
            if int(content_length) > 65_536:
                return Response(
                    content='{"ok":false,"detail":"Request body too large"}',
                    status_code=413,
                    media_type="application/json",
                )
        except (ValueError, TypeError):
            pass

    from services.mesh.mesh_hashchain import gate_store

    body_bytes = await request.body()
    if not _verify_peer_push_hmac(request, body_bytes):
        return Response(
            content='{"ok":false,"detail":"Invalid or missing peer HMAC"}',
            status_code=403,
            media_type="application/json",
        )

    body = json_mod.loads(body_bytes or b"{}")
    gate_id = str(body.get("gate_id", "") or "").strip().lower()
    after_count = _safe_int(body.get("after_count", 0) or 0)

    if not gate_id:
        # If no gate_id, return all known gate IDs with their event counts
        # so the puller knows which gates to sync.
        gate_ids = gate_store.known_gate_ids()
        gate_counts: dict[str, int] = {}
        for gid in gate_ids:
            with gate_store._lock:
                gate_counts[gid] = len(gate_store._gates.get(gid, []))
        return {"ok": True, "gates": gate_counts}

    with gate_store._lock:
        all_events = list(gate_store._gates.get(gate_id, []))
    total = len(all_events)
    if after_count >= total:
        return {"ok": True, "events": [], "total": total, "gate_id": gate_id}

    batch = all_events[after_count : after_count + _PEER_PUSH_BATCH_SIZE]
    return {"ok": True, "events": batch, "total": total, "gate_id": gate_id}


# ---------------------------------------------------------------------------
# Peer Management API ├óΓé¼ΓÇ¥ operator endpoints for adding / removing / listing
# peers without editing peer_store.json by hand.
# ---------------------------------------------------------------------------


@app.get("/api/mesh/peers", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def list_peers(request: Request, bucket: str = Query(None)):
    """List all peers (or filter by bucket: sync, push, bootstrap)."""
    from services.mesh.mesh_peer_store import DEFAULT_PEER_STORE_PATH, PeerStore

    store = PeerStore(DEFAULT_PEER_STORE_PATH)
    try:
        store.load()
    except Exception as exc:
        return {"ok": False, "detail": f"Failed to load peer store: {exc}"}

    if bucket:
        records = store.records_for_bucket(bucket)
    else:
        records = store.records()

    return {
        "ok": True,
        "count": len(records),
        "peers": [r.to_dict() for r in records],
    }


@app.post("/api/mesh/peers", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
@mesh_write_exempt(MeshWriteExemption.LOCAL_OPERATOR_ONLY)
async def add_peer(request: Request):
    """Add a peer to the store. Body: {peer_url, transport?, label?, role?, buckets?[]}."""
    from services.mesh.mesh_crypto import normalize_peer_url
    from services.mesh.mesh_peer_store import (
        DEFAULT_PEER_STORE_PATH,
        PeerStore,
        PeerStoreError,
        make_push_peer_record,
        make_sync_peer_record,
    )
    from services.mesh.mesh_router import peer_transport_kind

    body = await request.json()
    peer_url_raw = str(body.get("peer_url", "") or "").strip()
    if not peer_url_raw:
        return {"ok": False, "detail": "peer_url is required"}

    peer_url = normalize_peer_url(peer_url_raw)
    if not peer_url:
        return {"ok": False, "detail": "Invalid peer_url"}

    transport = str(body.get("transport", "") or "").strip().lower()
    if not transport:
        transport = peer_transport_kind(peer_url)
    if not transport:
        return {"ok": False, "detail": "Cannot determine transport for peer_url ├óΓé¼ΓÇ¥ provide transport explicitly"}

    label = str(body.get("label", "") or "").strip()
    role = str(body.get("role", "") or "").strip().lower() or "relay"
    buckets = body.get("buckets", ["sync", "push"])
    if isinstance(buckets, str):
        buckets = [buckets]
    if not isinstance(buckets, list):
        buckets = ["sync", "push"]

    store = PeerStore(DEFAULT_PEER_STORE_PATH)
    try:
        store.load()
    except Exception:
        store = PeerStore(DEFAULT_PEER_STORE_PATH)

    added: list[str] = []
    try:
        for b in buckets:
            b = str(b).strip().lower()
            if b == "sync":
                store.upsert(make_sync_peer_record(peer_url=peer_url, transport=transport, role=role, label=label))
                added.append("sync")
            elif b == "push":
                store.upsert(make_push_peer_record(peer_url=peer_url, transport=transport, role=role, label=label))
                added.append("push")
        store.save()
    except PeerStoreError as exc:
        return {"ok": False, "detail": str(exc)}

    return {"ok": True, "peer_url": peer_url, "buckets": added}


@app.delete("/api/mesh/peers", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
@mesh_write_exempt(MeshWriteExemption.LOCAL_OPERATOR_ONLY)
async def remove_peer(request: Request):
    """Remove a peer. Body: {peer_url, bucket?}. If bucket omitted, removes from all buckets."""
    from services.mesh.mesh_crypto import normalize_peer_url
    from services.mesh.mesh_peer_store import DEFAULT_PEER_STORE_PATH, PeerStore

    body = await request.json()
    peer_url_raw = str(body.get("peer_url", "") or "").strip()
    if not peer_url_raw:
        return {"ok": False, "detail": "peer_url is required"}

    peer_url = normalize_peer_url(peer_url_raw)
    if not peer_url:
        return {"ok": False, "detail": "Invalid peer_url"}

    bucket_filter = str(body.get("bucket", "") or "").strip().lower()

    store = PeerStore(DEFAULT_PEER_STORE_PATH)
    try:
        store.load()
    except Exception:
        return {"ok": False, "detail": "Failed to load peer store"}

    removed: list[str] = []
    for b in ["bootstrap", "sync", "push"]:
        if bucket_filter and b != bucket_filter:
            continue
        key = f"{b}:{peer_url}"
        if key in store._records:
            del store._records[key]
            removed.append(b)

    if not removed:
        return {"ok": False, "detail": "Peer not found in any bucket"}

    store.save()
    return {"ok": True, "peer_url": peer_url, "removed_from": removed}


@app.patch("/api/mesh/peers", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
@mesh_write_exempt(MeshWriteExemption.LOCAL_OPERATOR_ONLY)
async def toggle_peer(request: Request):
    """Enable or disable a peer. Body: {peer_url, bucket, enabled: bool}."""
    from services.mesh.mesh_crypto import normalize_peer_url
    from services.mesh.mesh_peer_store import DEFAULT_PEER_STORE_PATH, PeerRecord, PeerStore

    body = await request.json()
    peer_url_raw = str(body.get("peer_url", "") or "").strip()
    bucket = str(body.get("bucket", "") or "").strip().lower()
    enabled = body.get("enabled")

    if not peer_url_raw:
        return {"ok": False, "detail": "peer_url is required"}
    if not bucket:
        return {"ok": False, "detail": "bucket is required"}
    if enabled is None:
        return {"ok": False, "detail": "enabled (true/false) is required"}

    peer_url = normalize_peer_url(peer_url_raw)
    if not peer_url:
        return {"ok": False, "detail": "Invalid peer_url"}

    store = PeerStore(DEFAULT_PEER_STORE_PATH)
    try:
        store.load()
    except Exception:
        return {"ok": False, "detail": "Failed to load peer store"}

    key = f"{bucket}:{peer_url}"
    record = store._records.get(key)
    if not record:
        return {"ok": False, "detail": f"Peer not found in {bucket} bucket"}

    updated = PeerRecord(**{**record.to_dict(), "enabled": bool(enabled), "updated_at": int(time.time())})
    store._records[key] = updated
    store.save()

    return {"ok": True, "peer_url": peer_url, "bucket": bucket, "enabled": bool(enabled)}


@app.put("/api/mesh/gate/{gate_id}/envelope_policy")
@limiter.limit("10/minute")
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
async def set_gate_envelope_policy(request: Request, gate_id: str):
    """Set the envelope_policy for a gate. Requires gate admin scope."""
    ok, detail = _check_scoped_auth(request, "gate")
    if not ok:
        return Response(
            content='{"ok":false,"detail":"Gate admin scope required"}',
            status_code=403,
            media_type="application/json",
        )
    try:
        body = await request.json()
    except Exception:
        return {"ok": False, "detail": "Invalid JSON body"}
    policy = str(body.get("envelope_policy", "") or "").strip()
    acknowledge_recovery_risk = bool(body.get("acknowledge_recovery_risk", False))
    from services.mesh.mesh_reputation import gate_manager, VALID_ENVELOPE_POLICIES
    if policy not in VALID_ENVELOPE_POLICIES:
        return {"ok": False, "detail": f"Invalid policy: must be one of {VALID_ENVELOPE_POLICIES}"}
    success, msg = gate_manager.set_envelope_policy(
        gate_id,
        policy,
        acknowledge_recovery_risk=acknowledge_recovery_risk,
    )
    return {"ok": success, "detail": msg}


@app.put("/api/mesh/gate/{gate_id}/legacy_envelope_fallback")
@limiter.limit("10/minute")
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
async def set_gate_legacy_envelope_fallback(request: Request, gate_id: str):
    """Set legacy_envelope_fallback for a gate. Requires gate admin scope."""
    ok, detail = _check_scoped_auth(request, "gate")
    if not ok:
        return Response(
            content='{"ok":false,"detail":"Gate admin scope required"}',
            status_code=403,
            media_type="application/json",
        )
    try:
        body = await request.json()
    except Exception:
        return {"ok": False, "detail": "Invalid JSON body"}
    raw = body.get("legacy_envelope_fallback")
    acknowledge_legacy_risk = body.get("acknowledge_legacy_risk", False)
    if raw is None or not isinstance(raw, bool):
        return {"ok": False, "detail": "legacy_envelope_fallback must be a boolean"}
    if acknowledge_legacy_risk is not None and not isinstance(acknowledge_legacy_risk, bool):
        return {"ok": False, "detail": "acknowledge_legacy_risk must be a boolean"}
    from services.mesh.mesh_reputation import gate_manager
    success, msg = gate_manager.set_legacy_envelope_fallback(
        gate_id,
        raw,
        acknowledge_legacy_risk=bool(acknowledge_legacy_risk),
    )
    return {"ok": success, "detail": msg}


@app.get("/api/mesh/gate/{gate_id}/messages")
@limiter.limit("60/minute")
async def gate_messages(
    request: Request,
    gate_id: str,
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    """Get encrypted gate messages from private store (newest first). Requires gate membership."""
    access = _verify_gate_access(request, gate_id)
    if not access:
        return await _private_plane_refusal_response(
            request,
            status_code=403,
            payload=_private_plane_access_denied_payload(),
        )
    return _build_gate_message_response(gate_id, access, limit=limit, offset=offset)


def _build_gate_message_response(
    gate_id: str,
    access: str,
    *,
    limit: int = 20,
    offset: int = 0,
) -> dict[str, Any]:
    from services.mesh.mesh_hashchain import gate_store
    from services.mesh.mesh_reputation import gate_manager

    raw_messages, cursor = gate_store.get_messages_with_cursor(gate_id, limit=limit, offset=offset)
    safe_messages = [_strip_gate_for_access(m, access) for m in raw_messages]
    if gate_id and not safe_messages:
        gate_meta = gate_manager.get_gate(gate_id)
        if gate_meta:
            welcome_text = str(gate_meta.get("welcome") or gate_meta.get("description") or "").strip()
            if welcome_text:
                safe_messages = [
                    {
                        "event_id": f"seed_{gate_id}_welcome",
                        "event_type": "gate_notice",
                        "node_id": "!sb_gate",
                        "message": welcome_text,
                        "gate": gate_id,
                        "timestamp": int(gate_meta.get("created_at") or time.time()),
                        "sequence": 0,
                        "ephemeral": False,
                        "system_seed": True,
                        "fixed_gate": bool(gate_meta.get("fixed", False)),
                    }
                ]
    return {"messages": safe_messages, "count": len(safe_messages), "gate": gate_id, "cursor": cursor}


@app.get("/api/mesh/infonet/messages")
@limiter.limit("60/minute")
async def infonet_messages(
    request: Request,
    gate: str = "",
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    """Browse messages on the Infonet (newest first). Optional gate filter."""
    from services.mesh.mesh_hashchain import infonet

    if gate:
        access = _verify_gate_access(request, gate)
        if not access:
            return await _private_plane_refusal_response(
                request,
                status_code=403,
                payload=_private_plane_access_denied_payload(),
            )
        return _build_gate_message_response(gate, access, limit=limit, offset=offset)
    else:
        messages = infonet.get_messages(gate_id="", limit=limit, offset=offset)
        messages = [m for m in messages if m.get("event_type") != "gate_message"]
        messages = [_redact_public_event(m) for m in messages]
    return {"messages": messages, "count": len(messages), "gate": gate or "all", "cursor": 0}


@app.get("/api/mesh/infonet/messages/wait")
@limiter.limit("60/minute")
async def infonet_messages_wait(
    request: Request,
    gate: str = "",
    after: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    timeout_ms: int = Query(25_000, ge=1_000, le=90_000),
):
    """Wait for gate message changes, then return the latest gate view."""
    gate_id = str(gate or "").strip().lower()
    if not gate_id:
        return Response(
            content='{"ok":false,"detail":"gate required"}',
            status_code=400,
            media_type="application/json",
        )
    access = _verify_gate_access(request, gate_id)
    if not access:
        return await _private_plane_refusal_response(
            request,
            status_code=403,
            payload=_private_plane_access_denied_payload(),
        )
    from services.mesh.mesh_hashchain import gate_store

    changed, _cursor = await asyncio.to_thread(
        gate_store.wait_for_gate_change,
        gate_id,
        after,
        timeout_ms / 1000.0,
    )
    payload = _build_gate_message_response(gate_id, access, limit=limit, offset=0)
    payload["changed"] = bool(changed)
    return payload


@app.get("/api/mesh/infonet/event/{event_id}")
@limiter.limit("60/minute")
async def infonet_event(request: Request, event_id: str):
    """Look up a single Infonet event by ID."""
    from services.mesh.mesh_hashchain import gate_store, infonet

    evt = infonet.get_event(event_id)
    if not evt:
        evt = gate_store.get_event(event_id)
        if evt:
            gate_id = str(evt.get("payload", {}).get("gate", "") or evt.get("gate", "") or "").strip()
            access = _verify_gate_access(request, gate_id) if gate_id else ""
            if not gate_id or not access:
                return await _private_plane_refusal_response(
                    request,
                    status_code=403,
                    payload=_private_plane_access_denied_payload(),
                )
            return _strip_gate_for_access(evt, access)
        return {"ok": False, "detail": "Event not found"}
    if evt.get("event_type") == "dm_message":
        return await _private_plane_refusal_response(
            request,
            status_code=403,
            payload=_private_plane_access_denied_payload(),
        )
    if evt.get("event_type") == "gate_message":
        gate_id = str(evt.get("payload", {}).get("gate", "") or evt.get("gate", "") or "").strip()
        access = _verify_gate_access(request, gate_id) if gate_id else ""
        if not gate_id or not access:
            return await _private_plane_refusal_response(
                request,
                status_code=403,
                payload=_private_plane_access_denied_payload(),
            )
        return _strip_gate_for_access(evt, access)
    return _redact_public_event(infonet.decorate_event(evt))


@app.get("/api/mesh/infonet/node/{node_id}")
@limiter.limit("30/minute")
async def infonet_node_events(
    request: Request,
    node_id: str,
    limit: int = Query(20, ge=1, le=100),
):
    """Get recent Infonet events by a specific node."""
    from services.mesh.mesh_hashchain import infonet

    events = infonet.get_events_by_node(node_id, limit=limit)
    events = [e for e in events if e.get("event_type") not in {"gate_message", "dm_message"}]
    events = [_redact_public_event(e) for e in infonet.decorate_events(events)]
    events = _redact_public_node_history(
        events,
        authenticated=_scoped_view_authenticated(request, "mesh.audit"),
    )
    return {"events": events, "count": len(events), "node_id": node_id}


@app.get("/api/mesh/infonet/events")
@limiter.limit("30/minute")
async def infonet_events_by_type(
    request: Request,
    event_type: str = "",
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    """Get recent Infonet events, optionally filtered by type."""
    from services.mesh.mesh_hashchain import infonet

    if event_type:
        events = infonet.get_events_by_type(event_type, limit=limit, offset=offset)
    else:
        events = list(reversed(infonet.events))
        events = events[offset : offset + limit]
    events = [e for e in events if e.get("event_type") not in {"gate_message", "dm_message"}]
    events = [_redact_public_event(e) for e in infonet.decorate_events(events)]
    return {
        "events": events,
        "count": len(events),
        "event_type": event_type or "all",
    }


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Oracle Endpoints ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼


@app.post("/api/mesh/oracle/predict")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.ORACLE_PREDICT)
async def oracle_predict(request: Request):
    """Place a prediction on a market outcome. FINAL decision.

    Body: {node_id, market_title, side, stake_amount?: number}
    - stake_amount = 0 or omitted ├óΓÇáΓÇÖ FREE PICK (earn rep if correct)
    - stake_amount > 0 ├óΓÇáΓÇÖ STAKE REP (risk rep, split loser pool if correct)
    - side can be "yes"/"no" or an outcome name for multi-outcome markets
    """
    from services.mesh.mesh_oracle import oracle_ledger

    body = _signed_body(request)
    node_id = body.get("node_id", "")
    market_title = body.get("market_title", "")
    side = body.get("side", "")
    stake_amount = _safe_float(body.get("stake_amount", 0))
    public_key = body.get("public_key", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("signature", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")

    if not node_id or not market_title or not side:
        return {"ok": False, "detail": "Missing node_id, market_title, or side"}

    prediction_payload = {
        "market_title": market_title,
        "side": side,
        "stake_amount": stake_amount,
    }
    try:
        from services.mesh.mesh_reputation import reputation_ledger

        reputation_ledger.register_node(node_id, public_key, public_key_algo)
    except Exception:
        pass

    # Get current market probability from live data
    data = get_latest_data()
    markets = data.get("prediction_markets", [])
    matched = None
    for m in markets:
        if m.get("title", "").lower() == market_title.lower():
            matched = m
            break
    # Fuzzy fallback ├óΓé¼ΓÇ¥ partial match
    if not matched:
        for m in markets:
            if market_title.lower() in m.get("title", "").lower():
                matched = m
                break

    if not matched:
        return {"ok": False, "detail": f"Market '{market_title}' not found in active markets."}

    # Determine probability for the chosen side
    # For binary yes/no, use consensus_pct. For multi-outcome, find the outcome's pct.
    probability = 50.0
    side_lower = side.lower()
    outcomes = matched.get("outcomes", [])
    if outcomes:
        # Multi-outcome: find the specific outcome's probability
        for o in outcomes:
            if o.get("name", "").lower() == side_lower:
                probability = float(o.get("pct", 50))
                break
    else:
        # Binary market
        consensus = matched.get("consensus_pct")
        if consensus is None:
            consensus = matched.get("polymarket_pct") or matched.get("kalshi_pct") or 50
        probability = float(consensus)
        if side_lower == "no":
            probability = 100.0 - probability

    if stake_amount > 0:
        # STAKED prediction ├óΓé¼ΓÇ¥ risk rep for bigger reward
        ok, detail = oracle_ledger.place_market_stake(
            node_id, matched["title"], side, stake_amount, probability
        )
        mode = "staked"
    else:
        # FREE prediction ├óΓé¼ΓÇ¥ no rep risked
        ok, detail = oracle_ledger.place_prediction(node_id, matched["title"], side, probability)
        mode = "free"

    # Record on Infonet
    if ok:
        try:
            from services.mesh.mesh_hashchain import infonet

            normalized_payload = normalize_payload("prediction", prediction_payload)
            infonet.append(
                event_type="prediction",
                node_id=node_id,
                payload=normalized_payload,
                signature=signature,
                sequence=sequence,
                public_key=public_key,
                public_key_algo=public_key_algo,
                protocol_version=protocol_version,
            )
        except Exception:
            pass

    return {"ok": ok, "detail": detail, "probability": probability, "mode": mode}


@app.get("/api/mesh/oracle/markets")
@limiter.limit("30/minute")
async def oracle_markets(request: Request):
    """List active prediction markets, categorized with top 10 per category.
    Includes network consensus data (picks + staked rep per side)."""
    from collections import defaultdict
    from services.mesh.mesh_oracle import oracle_ledger

    data = get_latest_data()
    markets = data.get("prediction_markets", [])

    # Get consensus for all active markets (bulk)
    all_consensus = oracle_ledger.get_all_market_consensus()

    by_category = defaultdict(list)
    for m in markets:
        by_category[m.get("category", "NEWS")].append(m)

    _fields = (
        "title",
        "consensus_pct",
        "polymarket_pct",
        "kalshi_pct",
        "volume",
        "volume_24h",
        "end_date",
        "description",
        "category",
        "sources",
        "slug",
        "kalshi_ticker",
        "outcomes",
        "kalshi_volume",
    )
    categories = {}
    cat_totals = {}
    for cat in ["POLITICS", "CONFLICT", "NEWS", "FINANCE", "CRYPTO", "SPORTS"]:
        all_cat = sorted(
            by_category.get(cat, []),
            key=lambda x: x.get("volume", 0) or 0,
            reverse=True,
        )
        cat_totals[cat] = len(all_cat)
        cat_list = []
        for m in all_cat[:10]:
            entry = {k: m.get(k) for k in _fields}
            entry["consensus"] = all_consensus.get(m.get("title", ""), {})
            cat_list.append(entry)
        categories[cat] = cat_list

    return {"categories": categories, "total_count": len(markets), "cat_totals": cat_totals}


@app.get("/api/mesh/oracle/search")
@limiter.limit("20/minute")
async def oracle_search(request: Request, q: str = "", limit: int = 20, offset: int = 0):
    """Search prediction markets across Polymarket and Kalshi provider APIs."""
    if not q or len(q) < 2:
        return {"results": [], "query": q, "count": 0, "offset": offset, "has_more": False}

    from services.fetchers.prediction_markets import search_kalshi_direct, search_polymarket_direct

    limit = max(1, min(int(limit or 20), 100))
    offset = max(0, int(offset or 0))
    provider_limit = offset + limit + 25

    # Search both providers directly. Kalshi does not expose a reliable public
    # text-search parameter, so the fetcher performs bounded cursor scans.
    poly_results = search_polymarket_direct(q, limit=provider_limit, offset=0)
    kalshi_results = search_kalshi_direct(q, limit=provider_limit, offset=0)

    # Also search cached merged data so cross-provider consensus entries win.
    data = get_latest_data()
    markets = data.get("prediction_markets", [])
    q_lower = q.lower()
    cached_matches = [m for m in markets if q_lower in m.get("title", "").lower()]

    # Deduplicate: prefer cached merged rows, then provider-native rows.
    seen_titles = set()
    combined = []
    for m in cached_matches:
        seen_titles.add(m["title"].lower())
        combined.append(m)
    for m in [*poly_results, *kalshi_results]:
        if m["title"].lower() not in seen_titles:
            seen_titles.add(m["title"].lower())
            combined.append(m)

    # Sort by volume descending
    combined.sort(key=lambda x: x.get("volume", 0) or 0, reverse=True)

    _fields = (
        "title",
        "consensus_pct",
        "polymarket_pct",
        "kalshi_pct",
        "volume",
        "volume_24h",
        "end_date",
        "description",
        "category",
        "sources",
        "slug",
        "kalshi_ticker",
        "outcomes",
        "kalshi_volume",
    )
    page = combined[offset : offset + limit]
    results = [{k: m.get(k) for k in _fields} for m in page]
    return {
        "results": results,
        "query": q,
        "count": len(results),
        "offset": offset,
        "has_more": len(combined) > offset + limit,
        "total_seen": len(combined),
    }


@app.get("/api/mesh/oracle/markets/more")
@limiter.limit("30/minute")
async def oracle_markets_more(
    request: Request, category: str = "NEWS", offset: int = 0, limit: int = 10
):
    """Load more markets for a specific category (paginated)."""
    category = (category or "NEWS").upper()
    offset = max(0, int(offset or 0))
    limit = max(1, min(int(limit or 10), 100))
    data = get_latest_data()
    markets = data.get("prediction_markets", [])
    cat_markets = sorted(
        [m for m in markets if category == "ALL" or m.get("category") == category],
        key=lambda x: x.get("volume", 0) or 0,
        reverse=True,
    )

    page = cat_markets[offset : offset + limit]
    _fields = (
        "title",
        "consensus_pct",
        "polymarket_pct",
        "kalshi_pct",
        "volume",
        "volume_24h",
        "end_date",
        "description",
        "category",
        "sources",
        "slug",
        "kalshi_ticker",
        "outcomes",
        "kalshi_volume",
    )
    results = [{k: m.get(k) for k in _fields} for m in page]
    return {
        "markets": results,
        "category": category,
        "offset": offset,
        "has_more": offset + limit < len(cat_markets),
        "total": len(cat_markets),
    }


@app.post("/api/mesh/oracle/resolve")
@limiter.limit("5/minute")
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
async def oracle_resolve(request: Request):
    """Resolve a prediction market (admin/agent action).

    Body: {market_title, outcome: "yes"|"no" or any outcome name}
    """
    from services.mesh.mesh_oracle import oracle_ledger

    body = await request.json()
    market_title = body.get("market_title", "")
    outcome = body.get("outcome", "")

    if not market_title or not outcome:
        return {"ok": False, "detail": "Need market_title and outcome"}

    # Resolve free predictions
    winners, losers = oracle_ledger.resolve_market(market_title, outcome)
    # Resolve market stakes
    stake_result = oracle_ledger.resolve_market_stakes(market_title, outcome)

    return {
        "ok": True,
        "detail": f"Resolved: {winners} free winners, {losers} free losers, "
        f"{stake_result.get('winners', 0)} stake winners, {stake_result.get('losers', 0)} stake losers",
        "free": {"winners": winners, "losers": losers},
        "stakes": stake_result,
    }


@app.get("/api/mesh/oracle/consensus")
@limiter.limit("30/minute")
async def oracle_consensus(request: Request, market_title: str = ""):
    """Get network consensus for a market ├óΓé¼ΓÇ¥ picks + staked rep per side."""
    from services.mesh.mesh_oracle import oracle_ledger

    if not market_title:
        return {"error": "market_title required"}
    return oracle_ledger.get_market_consensus(market_title)


@app.post("/api/mesh/oracle/stake")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.ORACLE_STAKE)
async def oracle_stake(request: Request):
    """Stake oracle rep on a post's truthfulness.

    Body: {staker_id, message_id, poster_id, side: "truth"|"false", amount, duration_days: 1-7}
    """
    from services.mesh.mesh_oracle import oracle_ledger

    body = _signed_body(request)
    staker_id = body.get("staker_id", "")
    message_id = body.get("message_id", "")
    poster_id = body.get("poster_id", "")
    side = body.get("side", "").lower()
    amount = _safe_float(body.get("amount", 0))
    duration_days = _safe_int(body.get("duration_days", 1), 1)
    public_key = body.get("public_key", "")
    public_key_algo = body.get("public_key_algo", "")
    signature = body.get("signature", "")
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "")

    if not staker_id or not message_id or not side:
        return {"ok": False, "detail": "Missing staker_id, message_id, or side"}

    stake_payload = {
        "message_id": message_id,
        "poster_id": poster_id,
        "side": side,
        "amount": amount,
        "duration_days": duration_days,
    }
    try:
        from services.mesh.mesh_reputation import reputation_ledger

        reputation_ledger.register_node(staker_id, public_key, public_key_algo)
    except Exception:
        pass

    ok, detail = oracle_ledger.place_stake(
        staker_id, message_id, poster_id, side, amount, duration_days
    )

    # Record on Infonet
    if ok:
        try:
            from services.mesh.mesh_hashchain import infonet

            normalized_payload = normalize_payload("stake", stake_payload)
            infonet.append(
                event_type="stake",
                node_id=staker_id,
                payload=normalized_payload,
                signature=signature,
                sequence=sequence,
                public_key=public_key,
                public_key_algo=public_key_algo,
                protocol_version=protocol_version,
            )
        except Exception:
            pass

    return {"ok": ok, "detail": detail}


@app.get("/api/mesh/oracle/stakes/{message_id}")
@limiter.limit("30/minute")
async def oracle_stakes_for_message(request: Request, message_id: str):
    """Get all oracle stakes on a message."""
    from services.mesh.mesh_oracle import oracle_ledger

    return _redact_public_oracle_stakes(
        oracle_ledger.get_stakes_for_message(message_id),
        authenticated=_scoped_view_authenticated(request, "mesh.audit"),
    )


@app.get("/api/mesh/oracle/profile")
@limiter.limit("30/minute")
async def oracle_profile(request: Request, node_id: str = ""):
    """Get full oracle profile ├óΓé¼ΓÇ¥ rep, prediction history, win rate, farming score."""
    from services.mesh.mesh_oracle import oracle_ledger

    if not node_id:
        return {"ok": False, "detail": "Provide ?node_id=xxx"}
    profile = oracle_ledger.get_oracle_profile(node_id)
    return _redact_public_oracle_profile(
        profile,
        authenticated=_scoped_view_authenticated(request, "mesh.audit"),
    )


@app.get("/api/mesh/oracle/predictions")
@limiter.limit("30/minute")
async def oracle_predictions(request: Request, node_id: str = ""):
    """Get a node's active (unresolved) predictions."""
    from services.mesh.mesh_oracle import oracle_ledger

    if not node_id:
        return {"ok": False, "detail": "Provide ?node_id=xxx"}
    active_predictions = oracle_ledger.get_active_predictions(node_id)
    return _redact_public_oracle_predictions(
        active_predictions,
        authenticated=_scoped_view_authenticated(request, "mesh.audit"),
    )


@app.post("/api/mesh/oracle/resolve-stakes")
@limiter.limit("5/minute")
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
async def oracle_resolve_stakes(request: Request):
    """Resolve all expired stake contests. Can be called periodically or manually."""
    from services.mesh.mesh_oracle import oracle_ledger

    resolutions = oracle_ledger.resolve_expired_stakes()
    return {"ok": True, "resolutions": resolutions, "count": len(resolutions)}


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼ Encrypted DM Relay (Dead Drop) ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼


def _secure_dm_enabled() -> bool:
    return bool(get_settings().MESH_DM_SECURE_MODE)


def _legacy_dm_get_allowed() -> bool:
    return bool(legacy_dm_get_override_active())


def _legacy_dm1_allowed() -> bool:
    return bool(legacy_dm1_override_active())


def _rns_private_dm_ready() -> bool:
    try:
        from services.mesh.mesh_rns import rns_bridge

        return bool(rns_bridge.enabled()) and bool(rns_bridge.status().get("private_dm_direct_ready"))
    except Exception:
        return False


def _anonymous_dm_hidden_transport_enforced() -> bool:
    state = _anonymous_mode_state()
    return bool(state.get("enabled")) and bool(state.get("ready"))


def _anonymous_dm_hidden_transport_requested() -> bool:
    """User has asked for anonymous mode, regardless of whether hidden transport
    is *ready* yet.

    Use this (not the ``_enforced`` variant) for *protective* logic that must
    keep stated privacy intent honored during warmup ΓÇö e.g., skipping direct
    RNS metadata lookups. ``_enforced`` is for claim/telemetry paths that
    report what is currently being honored.
    """
    return bool(_anonymous_mode_state().get("enabled"))


def _high_privacy_profile_enabled() -> bool:
    try:
        from services.wormhole_settings import read_wormhole_settings

        settings = read_wormhole_settings()
        return str(settings.get("privacy_profile", "default") or "default").lower() == "high"
    except Exception:
        return False


async def _maybe_apply_dm_relay_jitter() -> None:
    # Hardening Rec #7b: apply a modest baseline jitter even in the default
    # privacy profile so DM send timing is not trivially fingerprintable.
    # "high" profile keeps the original 50-500 ms window; default profile
    # adds 0-20 ms which is imperceptible to users but disrupts fine-grained
    # timing correlation across concurrent requests.
    if _high_privacy_profile_enabled():
        await asyncio.sleep((50 + secrets.randbelow(451)) / 1000.0)
        return
    await asyncio.sleep(secrets.randbelow(21) / 1000.0)


async def _maybe_apply_dm_poll_jitter() -> None:
    # Poll/count endpoints are activity probes. Keep default latency nearly
    # invisible, but make high-privacy polling harder to align with network
    # observations and mailbox state changes.
    if _high_privacy_profile_enabled():
        await asyncio.sleep((100 + secrets.randbelow(901)) / 1000.0)
        return
    await asyncio.sleep(secrets.randbelow(26) / 1000.0)


def _dm_request_fresh(timestamp: int) -> bool:
    now_ts = int(time.time())
    max_age = max(30, int(get_settings().MESH_DM_REQUEST_MAX_AGE_S))
    return abs(timestamp - now_ts) <= max_age


def _validate_private_signed_sequence(
    infonet: Any,
    node_id: str,
    sequence: int,
    *,
    domain: str,
) -> tuple[bool, str]:
    """Advance replay state for a private signed side-effect domain.

    Older test doubles and older runtime objects only accept the historical
    two-argument form. In that case, fold the domain into the node key so
    cross-kind replay separation is still preserved.
    """
    normalized_domain = str(domain or "").strip().lower()
    try:
        return infonet.validate_and_set_sequence(
            node_id,
            sequence,
            domain=normalized_domain,
        )
    except TypeError:
        domain_key = f"{node_id}|{normalized_domain}" if normalized_domain else node_id
        return infonet.validate_and_set_sequence(domain_key, sequence)


def _wake_private_release_worker() -> None:
    private_release_worker.ensure_started()
    private_release_worker.wake()


def _queue_dm_release(*, current_tier: str, payload: dict[str, Any]) -> dict[str, Any]:
    item = private_delivery_outbox.enqueue(
        lane="dm",
        release_key=str(payload.get("msg_id", "") or ""),
        payload=payload,
        current_tier=current_tier,
        required_tier=release_lane_required_tier("dm"),
    )
    if evaluate_network_release("dm", current_tier).should_bootstrap:
        private_transport_manager.request_warmup(
            reason="queued_dm_delivery",
            current_tier=current_tier,
            required_tier=release_lane_required_tier("dm"),
        )
    _wake_private_release_worker()
    outbox_id = str(item.get("id", "") or "")
    auto_release: dict[str, Any] = {"ok": True, "skipped": True}
    if outbox_id:
        try:
            from services.mesh.mesh_dm_connect_delivery import auto_release_connect_dm_outbox

            auto_release = auto_release_connect_dm_outbox(outbox_id=outbox_id, payload=payload)
        except Exception as exc:
            auto_release = {"ok": False, "detail": str(exc) or type(exc).__name__}
    return {
        "ok": True,
        "msg_id": str(payload.get("msg_id", "") or ""),
        "outbox_id": outbox_id,
        "queued": True,
        "detail": str((item.get("status") or {}).get("label", "") or "Queued for private delivery"),
        "auto_release": auto_release,
        "delivery": {
            "state": canonical_release_state(str(item.get("release_state", "") or "queued")),
            "internal_state": str(item.get("release_state", "") or "queued"),
            "local_state": "sealed_local",
            "network_state": network_release_state(
                "dm",
                str(item.get("release_state", "") or "queued"),
                result=dict(item.get("result") or {}),
            ),
            "status": dict(item.get("status") or {}),
            "required_tier": str(item.get("required_tier", "") or ""),
            "current_tier": str(item.get("current_tier", "") or ""),
        },
    }


def _queue_gate_release(*, current_tier: str, gate_id: str, payload: dict[str, Any]) -> dict[str, Any]:
    item = private_delivery_outbox.enqueue(
        lane="gate",
        release_key=str(payload.get("event_id", "") or ""),
        payload=payload,
        current_tier=current_tier,
        required_tier=release_lane_required_tier("gate"),
    )
    if evaluate_network_release("gate", current_tier).should_bootstrap:
        private_transport_manager.request_warmup(
            reason="queued_gate_delivery",
            current_tier=current_tier,
            required_tier=release_lane_required_tier("gate"),
        )
    _wake_private_release_worker()
    return {
        "ok": True,
        "detail": str((item.get("status") or {}).get("label", "") or "Queued for private delivery"),
        "gate_id": gate_id,
        "event_id": str(payload.get("event_id", "") or ""),
        "outbox_id": str(item.get("id", "") or ""),
        "queued": True,
        "local_state": "sealed_local",
        "network_state": network_release_state(
            "gate",
            str(item.get("release_state", "") or "queued"),
            result=dict(item.get("result") or {}),
        ),
        "delivery": {
            "state": canonical_release_state(str(item.get("release_state", "") or "queued")),
            "internal_state": str(item.get("release_state", "") or "queued"),
            "local_state": "sealed_local",
            "network_state": network_release_state(
                "gate",
                str(item.get("release_state", "") or "queued"),
                result=dict(item.get("result") or {}),
            ),
            "status": dict(item.get("status") or {}),
            "required_tier": str(item.get("required_tier", "") or ""),
            "current_tier": str(item.get("current_tier", "") or ""),
        },
    }


def _normalize_mailbox_claims(mailbox_claims: list[dict]) -> list[dict]:
    normalized: list[dict] = []
    for claim in mailbox_claims[:32]:
        if not isinstance(claim, dict):
            continue
        normalized.append(
            {
                "type": str(claim.get("type", "")).lower(),
                "token": str(claim.get("token", "")),
            }
        )
    return normalized


def _verify_dm_mailbox_request(
    *,
    event_type: str,
    agent_id: str,
    mailbox_claims: list[dict],
    timestamp: int,
    nonce: str,
    public_key: str,
    public_key_algo: str,
    signature: str,
    sequence: int,
    protocol_version: str,
    skip_signature: bool = False,
):
    payload = {
        "mailbox_claims": _normalize_mailbox_claims(mailbox_claims),
        "timestamp": timestamp,
        "nonce": nonce,
    }
    valid, reason = validate_event_payload(event_type, payload)
    if not valid:
        return False, reason, payload
    if not skip_signature:
        sig_ok, sig_reason = _verify_signed_write(
            event_type=event_type,
            node_id=agent_id,
            sequence=sequence,
            public_key=public_key,
            public_key_algo=public_key_algo,
            signature=signature,
            payload=payload,
            protocol_version=protocol_version,
        )
        if not sig_ok:
            return False, sig_reason, payload
    if not _dm_request_fresh(timestamp):
        return False, "Mailbox request timestamp is stale", payload
    return True, "ok", payload


async def _dm_send_from_signed_request(request: Request):
    """Deposit an encrypted DM after decorator-level signed-write verification."""
    from services.wormhole_supervisor import get_transport_tier

    tier = get_transport_tier()
    transport_upgrade_pending = bool(getattr(request.state, "_dm_send_transport_pending", False))
    if tier == "public_degraded":
        transport_upgrade_pending = True
        if not bool(getattr(request.state, "_dm_send_transport_pending", False)):
            _kickoff_dm_send_transport_upgrade()

    # Hardening Rec #9: if anonymous mode is *requested* but hidden transport
    # has not converged to ready, queue the DM via the private release outbox
    # instead of falling through to direct/relay. Without this, a user who
    # flips anonymous_mode on during a warmup window could egress a DM over a
    # non-hidden transport, silently betraying the stated privacy intent. Non-
    # hostile per policy: the response carries private_transport_pending so
    # the client surfaces a "warming up" state rather than a hard deny.
    _anon_state = _anonymous_mode_state()
    if bool(_anon_state.get("enabled")) and not bool(_anon_state.get("ready")):
        transport_upgrade_pending = True
        if not bool(getattr(request.state, "_dm_send_transport_pending", False)):
            _kickoff_dm_send_transport_upgrade()

    prepared = _prepared_signed_write(request)
    body = _signed_body(request)
    sig_reason = str(prepared.reason if prepared is not None else "ok")
    sender_id = str(body.get("sender_id", "")).strip()
    sender_token_hash = str(
        ((prepared.extras if prepared is not None else {}) or {}).get("sender_token_hash", "")
        or body.get("sender_token_hash", "")
        or ""
    ).strip()
    recipient_id = str(body.get("recipient_id", "")).strip()
    delivery_class = str(body.get("delivery_class", "")).strip().lower()
    recipient_token = str(body.get("recipient_token", "")).strip()
    ciphertext = str(body.get("ciphertext", "")).strip()
    payload_format = str(body.get("format", "mls1") or "mls1").strip().lower() or "mls1"
    if str(tier or "").startswith("private_") and payload_format == "dm1":
        return JSONResponse(
            {"ok": False, "detail": "MLS session required in private transport mode - dm1 blocked on raw send path"},
            status_code=403,
        )
    session_welcome = str(body.get("session_welcome", "") or "").strip()
    sender_seal = str(body.get("sender_seal", "")).strip()
    relay_salt_hex = str(body.get("relay_salt", "") or "").strip().lower()
    msg_id = str(body.get("msg_id", "")).strip()
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    sequence = _safe_int(body.get("sequence", 0) or 0)
    nonce = str(body.get("nonce", "")).strip()

    if not sender_id or not recipient_id or not ciphertext or not msg_id or not timestamp:
        return {"ok": False, "detail": "Missing sender_id, recipient_id, ciphertext, msg_id, or timestamp"}
    now_ts = int(time.time())
    if abs(timestamp - now_ts) > 7 * 86400:
        return {"ok": False, "detail": "DM timestamp is too far from current time"}
    if delivery_class not in ("request", "shared"):
        return {"ok": False, "detail": "delivery_class must be request or shared"}
    # Contact requests are the first-contact handshake ΓÇö do not require prior verification.
    if delivery_class == "shared":
        try:
            from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement

            verified_first_contact = verified_first_contact_requirement(recipient_id)
            if not verified_first_contact.get("ok"):
                return {
                    "ok": False,
                    "detail": str(
                        verified_first_contact.get("detail", "")
                        or "signed invite or SAS verification required before secure first contact"
                    ),
                    "trust_level": str(verified_first_contact.get("trust_level", "") or "unpinned"),
                }
        except Exception:
            pass
    if (
        str(tier or "").startswith("private_")
        and delivery_class == "shared"
        and bool(get_settings().MESH_DM_REQUIRE_SENDER_SEAL_SHARED)
        and not sender_seal
    ):
        return {"ok": False, "detail": "sealed sender required for shared private DMs"}
    if delivery_class == "shared" and not recipient_token:
        return {"ok": False, "detail": "recipient_token required for shared delivery"}
    if delivery_class == "shared" and not sender_token_hash:
        return {"ok": False, "detail": "sender_token required for shared delivery"}
    if delivery_class == "request" and not sender_token_hash:
        return {"ok": False, "detail": "sender_token required for request delivery"}
    from services.mesh.mesh_dm_relay import dm_relay

    compat = _apply_legacy_dm_signature_compat(
        tier=tier,
        delivery_class=delivery_class,
        payload_format=payload_format,
        session_welcome=session_welcome,
        sender_seal=sender_seal,
        relay_salt_hex=relay_salt_hex,
        sig_reason=sig_reason,
    )
    if not compat["ok"]:
        if int(compat["status_code"] or 0) > 0:
            return JSONResponse({"ok": False, "detail": compat["detail"]}, status_code=int(compat["status_code"]))
        return {"ok": False, "detail": compat["detail"]}
    payload_format = str(compat["format"])
    session_welcome = str(compat["session_welcome"])
    sender_seal = str(compat["sender_seal"])
    relay_salt_hex = str(compat["relay_salt"])
    if str(tier or "").startswith("private_") and payload_format == "dm1":
        return JSONResponse(
            {"ok": False, "detail": "MLS session required in private transport mode - dm1 blocked on raw send path"},
            status_code=403,
        )

    send_nonce = nonce or msg_id
    nonce_ok, nonce_reason = dm_relay.consume_nonce(sender_id, send_nonce, timestamp)
    if not nonce_ok:
        return {"ok": False, "detail": nonce_reason}
    try:
        from services.mesh.mesh_hashchain import infonet

        ok_seq, seq_reason = _validate_private_signed_sequence(
            infonet,
            sender_id,
            sequence,
            domain="dm_send",
        )
        if not ok_seq:
            return {"ok": False, "detail": seq_reason}
    except Exception as exc:
        logger.warning("DM send sequence validation unavailable: %s", type(exc).__name__)

    if dm_relay.is_blocked(recipient_id, sender_id):
        return {"ok": False, "detail": "Recipient is not accepting your messages"}

    if sender_seal:
        if relay_salt_hex:
            if len(relay_salt_hex) != 32 or any(ch not in "0123456789abcdef" for ch in relay_salt_hex):
                return {"ok": False, "detail": "relay_salt must be a 32-character hex string"}
        else:
            import os as _os

            relay_salt_hex = _os.urandom(16).hex()

    connect_intent = str(body.get("connect_intent", "") or "").strip().lower()
    lookup_peer_url = str(body.get("lookup_peer_url", "") or "").strip().rstrip("/")
    release_payload = {
        "sender_id": sender_id,
        "sender_token_hash": sender_token_hash,
        "recipient_id": recipient_id,
        "delivery_class": delivery_class,
        "recipient_token": recipient_token if delivery_class == "shared" else "",
        "ciphertext": ciphertext,
        "format": payload_format,
        "session_welcome": session_welcome,
        "msg_id": msg_id,
        "timestamp": timestamp,
        "sender_seal": sender_seal,
        "relay_salt": relay_salt_hex,
    }
    if connect_intent:
        release_payload["connect_intent"] = connect_intent
    if lookup_peer_url:
        release_payload["lookup_peer_url"] = lookup_peer_url
    try:
        from services.mesh.mesh_dm_connect_delivery import enrich_connect_release_payload

        release_payload = enrich_connect_release_payload(release_payload)
    except Exception:
        pass
    hashchain_spool: dict[str, Any] = {"ok": False, "detail": "not attempted"}
    try:
        from services.mesh.mesh_hashchain import infonet

        chain_payload = dict(prepared.payload if prepared is not None else {})
        if not chain_payload:
            chain_payload = {
                "recipient_id": recipient_id,
                "delivery_class": delivery_class,
                "recipient_token": recipient_token if delivery_class == "shared" else "",
                "ciphertext": ciphertext,
                "msg_id": msg_id,
                "timestamp": timestamp,
                "format": payload_format,
            }
        chain_payload["transport_lock"] = "private_strong"
        if connect_intent:
            chain_payload["connect_intent"] = connect_intent
        if lookup_peer_url:
            chain_payload["lookup_peer_url"] = lookup_peer_url
        chain_event = infonet.append_private_dm_message(
            node_id=sender_id,
            payload=chain_payload,
            signature=str(prepared.signature if prepared is not None else body.get("signature", "") or ""),
            sequence=sequence,
            public_key=str(prepared.public_key if prepared is not None else body.get("public_key", "") or ""),
            public_key_algo=str(
                prepared.public_key_algo if prepared is not None else body.get("public_key_algo", "") or ""
            ),
            protocol_version=str(
                prepared.protocol_version if prepared is not None else body.get("protocol_version", "") or ""
            )
            or PROTOCOL_VERSION,
            timestamp=float(timestamp or time.time()),
        )
        # Relay deposit is deferred to the private release worker so scoped
        # connect traffic can synchronously replicate to lookup_peer_url once.
        hashchain_spool = {
            "ok": True,
            "event_id": str(chain_event.get("event_id", "") or ""),
            "limit": 2,
        }
    except Exception as exc:
        hashchain_spool = {"ok": False, "detail": str(exc) or type(exc).__name__}
    queued_result = _queue_dm_release(current_tier=tier, payload=release_payload)
    queued_result["hashchain_spool"] = hashchain_spool
    if transport_upgrade_pending:
        queued_result["private_transport_pending"] = True
    return queued_result

async def _dm_poll_secure_from_signed_request(request: Request):
    exposure = metadata_exposure_for_request(
        request,
        authenticated=_scoped_view_authenticated(request, "mesh"),
    )
    body = _signed_body(request)
    agent_id = str(body.get("agent_id", "")).strip()
    mailbox_claims = body.get("mailbox_claims", [])
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    nonce = str(body.get("nonce", "")).strip()
    public_key = str(body.get("public_key", "")).strip()
    public_key_algo = str(body.get("public_key_algo", "")).strip()
    signature = str(body.get("signature", "")).strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = str(body.get("protocol_version", "")).strip()
    if not agent_id:
        return dm_mailbox_response_view(
            {"ok": False, "detail": "Missing agent_id", "messages": [], "count": 0},
            exposure=exposure,
        )
    from services.mesh.mesh_dm_relay import dm_relay

    ok, reason, payload = _verify_dm_mailbox_request(
        event_type="dm_poll",
        agent_id=agent_id,
        mailbox_claims=mailbox_claims,
        timestamp=timestamp,
        nonce=nonce,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        sequence=sequence,
        protocol_version=protocol_version,
        skip_signature=True,
    )
    if not ok:
        return dm_mailbox_response_view(
            {"ok": False, "detail": reason, "messages": [], "count": 0},
            exposure=exposure,
        )
    nonce_ok, nonce_reason = dm_relay.consume_nonce(agent_id, nonce, timestamp)
    if not nonce_ok:
        return dm_mailbox_response_view(
            {"ok": False, "detail": nonce_reason, "messages": [], "count": 0},
            exposure=exposure,
        )
    try:
        from services.mesh.mesh_hashchain import infonet

        ok_seq, seq_reason = _validate_private_signed_sequence(
            infonet,
            agent_id,
            sequence,
            domain="dm_poll",
        )
        if not ok_seq:
            return dm_mailbox_response_view(
                {"ok": False, "detail": seq_reason, "messages": [], "count": 0},
                exposure=exposure,
            )
    except Exception:
        pass
    await _maybe_apply_dm_poll_jitter()
    claims = payload.get("mailbox_claims", [])
    mailbox_keys = dm_relay.claim_mailbox_keys(agent_id, claims)
    relay_msgs, relay_more = dm_relay.collect_claims(agent_id, claims, limit=DM_POLL_BATCH_LIMIT)
    relay_msgs = _annotate_request_recovery_messages(relay_msgs)
    direct_msgs: list[dict] = []
    direct_more = False
    direct_budget = DM_POLL_BATCH_LIMIT - len(relay_msgs)
    # Rec #9: use the *requested* helper so direct-lane metadata lookups are
    # skipped the moment a user opts into anonymous mode, not only after
    # hidden transport finishes warming up.
    if direct_budget > 0 and not _anonymous_dm_hidden_transport_requested():
        try:
            from services.mesh.mesh_rns import rns_bridge

            direct_msgs, direct_more = rns_bridge.collect_private_dm(mailbox_keys, limit=direct_budget)
            direct_msgs = _annotate_request_recovery_messages(direct_msgs)
        except Exception:
            direct_msgs = []
    elif direct_budget <= 0:
        direct_more = not _anonymous_dm_hidden_transport_requested()
    merged = _merge_dm_poll_messages(relay_msgs, direct_msgs)
    has_more = relay_more or direct_more
    msgs = merged[:DM_POLL_BATCH_LIMIT]
    return dm_mailbox_response_view(
        {"ok": True, "messages": msgs, "count": len(msgs), "has_more": has_more},
        exposure=exposure,
        diagnostic={
            "source_counts": {
                "relay": len(relay_msgs),
                "direct": len(direct_msgs),
                "returned": len(msgs),
            },
            "mailbox_claim_count": len(claims),
        },
    )


async def _dm_count_secure_from_signed_request(request: Request):
    exposure = metadata_exposure_for_request(
        request,
        authenticated=_scoped_view_authenticated(request, "mesh"),
    )
    body = _signed_body(request)
    agent_id = str(body.get("agent_id", "")).strip()
    mailbox_claims = body.get("mailbox_claims", [])
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    nonce = str(body.get("nonce", "")).strip()
    public_key = str(body.get("public_key", "")).strip()
    public_key_algo = str(body.get("public_key_algo", "")).strip()
    signature = str(body.get("signature", "")).strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = str(body.get("protocol_version", "")).strip()
    if not agent_id:
        return dm_mailbox_response_view(
            {"ok": False, "detail": "Missing agent_id", "count": 0},
            exposure=exposure,
        )
    from services.mesh.mesh_dm_relay import dm_relay

    ok, reason, payload = _verify_dm_mailbox_request(
        event_type="dm_count",
        agent_id=agent_id,
        mailbox_claims=mailbox_claims,
        timestamp=timestamp,
        nonce=nonce,
        public_key=public_key,
        public_key_algo=public_key_algo,
        signature=signature,
        sequence=sequence,
        protocol_version=protocol_version,
        skip_signature=True,
    )
    if not ok:
        return dm_mailbox_response_view(
            {"ok": False, "detail": reason, "count": 0},
            exposure=exposure,
        )
    nonce_ok, nonce_reason = dm_relay.consume_nonce(agent_id, nonce, timestamp)
    if not nonce_ok:
        return dm_mailbox_response_view(
            {"ok": False, "detail": nonce_reason, "count": 0},
            exposure=exposure,
        )
    try:
        from services.mesh.mesh_hashchain import infonet

        ok_seq, seq_reason = _validate_private_signed_sequence(
            infonet,
            agent_id,
            sequence,
            domain="dm_count",
        )
        if not ok_seq:
            return dm_mailbox_response_view(
                {"ok": False, "detail": seq_reason, "count": 0},
                exposure=exposure,
            )
    except Exception:
        pass
    await _maybe_apply_dm_poll_jitter()
    claims = payload.get("mailbox_claims", [])
    mailbox_keys = dm_relay.claim_mailbox_keys(agent_id, claims)
    relay_ids = dm_relay.claim_message_ids(agent_id, claims)
    direct_ids = set()
    # Rec #9: requested (not merely enforced) ΓÇö skip direct-lane count probe
    # as soon as anonymous mode is requested, even before ready converges.
    if not _anonymous_dm_hidden_transport_requested():
        try:
            from services.mesh.mesh_rns import rns_bridge

            direct_ids = rns_bridge.private_dm_ids(mailbox_keys)
        except Exception:
            direct_ids = set()
    exact_total = len(relay_ids | direct_ids)
    return dm_mailbox_response_view(
        {"ok": True, "count": _coarsen_dm_count(exact_total)},
        exposure=exposure,
        diagnostic={
            "source_counts": {
                "relay": len(relay_ids),
                "direct": len(direct_ids),
                "exact_total": exact_total,
            },
            "mailbox_claim_count": len(claims),
        },
    )


@app.post("/api/mesh/dm/register")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.DM_REGISTER)
async def dm_register_key(request: Request):
    """Register a DH public key for encrypted DM key exchange."""
    body = _signed_body(request)
    agent_id = body.get("agent_id", "").strip()
    dh_pub_key = body.get("dh_pub_key", "").strip()
    dh_algo = body.get("dh_algo", "").strip()
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    public_key = body.get("public_key", "").strip()
    public_key_algo = body.get("public_key_algo", "").strip()
    signature = body.get("signature", "").strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "").strip()
    if not agent_id or not dh_pub_key or not dh_algo or not timestamp:
        return {"ok": False, "detail": "Missing agent_id, dh_pub_key, dh_algo, or timestamp"}
    if dh_algo.upper() not in ("X25519", "ECDH_P256", "ECDH"):
        return {"ok": False, "detail": "Unsupported dh_algo"}
    now_ts = int(time.time())
    if abs(timestamp - now_ts) > 7 * 86400:
        return {"ok": False, "detail": "DH key timestamp is too far from current time"}
    from services.mesh.mesh_dm_relay import dm_relay

    try:
        from services.mesh.mesh_reputation import reputation_ledger

        reputation_ledger.register_node(agent_id, public_key, public_key_algo)
    except Exception:
        pass

    accepted, detail, metadata = dm_relay.register_dh_key(
        agent_id,
        dh_pub_key,
        dh_algo,
        timestamp,
        signature,
        public_key,
        public_key_algo,
        protocol_version,
        sequence,
    )
    if not accepted:
        return {"ok": False, "detail": detail}

    return {"ok": True, **(metadata or {})}


@app.get("/api/mesh/dm/pubkey")
@limiter.limit("30/minute")
async def dm_get_pubkey(
    request: Request,
    agent_id: str = "",
    lookup_token: str = "",
    lookup_peer_url: str = "",
):
    """Fetch an agent's DH public key for key exchange."""
    exposure = metadata_exposure_for_request(
        request,
        authenticated=_scoped_view_authenticated(request, "mesh"),
    )
    if not agent_id and not lookup_token:
        return dm_lookup_response_view(
            {"ok": False, "detail": "Missing agent_id or lookup_token"},
            exposure=exposure,
            lookup_token_present=bool(lookup_token),
        )
    from services.mesh.mesh_dm_relay import dm_relay

    resolved_id, resolved_lookup = _preferred_dm_lookup_target(agent_id, lookup_token)
    key_bundle = None
    lookup_mode = "legacy_agent_id"
    if resolved_lookup:
        key_bundle, resolved_id = dm_relay.get_dh_key_by_lookup(resolved_lookup)
        if key_bundle is None:
            # Invite handles are minted on the owner's node. When a remote peer
            # pastes a short address, resolve it across the private fleet before
            # failing ΓÇö same path as prekey-bundle import.
            from services.mesh.mesh_wormhole_prekey import fetch_dm_prekey_bundle

            preferred_lookup_peer = str(lookup_peer_url or "").strip().rstrip("/")
            remote_bundle = fetch_dm_prekey_bundle(
                agent_id="",
                lookup_token=resolved_lookup,
                lookup_peer_urls=[preferred_lookup_peer] if preferred_lookup_peer else None,
            )
            if remote_bundle.get("ok"):
                bundle = dict(remote_bundle.get("bundle") or remote_bundle)
                dh_pub = str(
                    bundle.get("identity_dh_pub_key", "")
                    or remote_bundle.get("identity_dh_pub_key", "")
                    or ""
                ).strip()
                if dh_pub:
                    resolved_id = str(remote_bundle.get("agent_id", "") or resolved_id or "").strip()
                    key_bundle = {
                        "dh_pub_key": dh_pub,
                        "dh_algo": str(remote_bundle.get("dh_algo", "X25519") or "X25519"),
                        "timestamp": int(remote_bundle.get("timestamp", 0) or 0),
                        "public_key": str(remote_bundle.get("public_key", "") or ""),
                        "public_key_algo": str(remote_bundle.get("public_key_algo", "") or ""),
                        "signature": str(remote_bundle.get("signature", "") or ""),
                        "sequence": int(remote_bundle.get("sequence", 0) or 0),
                        "prekey_transparency_head": str(
                            remote_bundle.get("prekey_transparency_head", "") or ""
                        ),
                        "prekey_transparency_size": int(
                            remote_bundle.get("prekey_transparency_size", 0) or 0
                        ),
                        "witness_count": int(remote_bundle.get("witness_count", 0) or 0),
                        "witness_latest_at": int(remote_bundle.get("witness_latest_at", 0) or 0),
                    }
            if key_bundle is None:
                return dm_lookup_response_view(
                    {"ok": False, "detail": "Agent not found or has no DH key", "lookup_mode": "invite_lookup_handle"},
                    exposure=exposure,
                    lookup_token_present=True,
                )
        lookup_mode = "invite_lookup_handle"
    if key_bundle is None and resolved_id:
        blocked = legacy_agent_id_lookup_blocked()
        record_legacy_agent_id_lookup(
            resolved_id,
            lookup_kind="dh_pubkey",
            blocked=blocked,
        )
        _warn_legacy_dm_pubkey_lookup(resolved_id)
        if blocked:
            return dm_lookup_response_view(
                {
                    "ok": False,
                    "detail": "legacy agent_id lookup disabled; use invite lookup handle",
                    "removal_target": sunset_target_label(LEGACY_AGENT_ID_LOOKUP_TARGET),
                },
                exposure=exposure,
                lookup_token_present=False,
            )
        key_bundle = dm_relay.get_dh_key(resolved_id)
    if key_bundle is None:
        return dm_lookup_response_view(
            {"ok": False, "detail": "Agent not found or has no DH key"},
            exposure=exposure,
            lookup_token_present=bool(resolved_lookup),
        )
    return dm_lookup_response_view(
        {"ok": True, "agent_id": resolved_id, "lookup_mode": lookup_mode, **key_bundle},
        exposure=exposure,
        lookup_token_present=bool(resolved_lookup),
    )


@app.get("/api/mesh/dm/prekey-bundle")
@limiter.limit("30/minute")
async def dm_get_prekey_bundle(
    request: Request,
    agent_id: str = "",
    lookup_token: str = "",
    lookup_peer_url: str = "",
):
    exposure = metadata_exposure_for_request(
        request,
        authenticated=_scoped_view_authenticated(request, "mesh"),
    )
    if not agent_id and not lookup_token:
        return dm_lookup_response_view(
            {"ok": False, "detail": "Missing agent_id or lookup_token"},
            exposure=exposure,
            lookup_token_present=bool(lookup_token),
        )
    resolved_id, resolved_lookup = _preferred_dm_lookup_target(agent_id, lookup_token)
    preferred_lookup_peer = str(lookup_peer_url or "").strip().rstrip("/")
    result = fetch_dm_prekey_bundle(
        agent_id=resolved_id,
        lookup_token=resolved_lookup,
        lookup_peer_urls=[preferred_lookup_peer] if preferred_lookup_peer else None,
    )
    return dm_lookup_response_view(
        result,
        exposure=exposure,
        lookup_token_present=bool(resolved_lookup),
    )


@app.post("/api/mesh/dm/send")
@limiter.limit("20/minute")
@requires_signed_write(kind=SignedWriteKind.DM_SEND)
async def dm_send(request: Request):
    return await _dm_send_from_signed_request(request)

_REQUEST_V2_REDUCED_VERSION = "request-v2-reduced-v3"
_REQUEST_V2_RECOVERY_STATES = {"pending", "verified", "failed"}


def _is_canonical_reduced_request_message(message: dict[str, Any]) -> bool:
    item = dict(message or {})
    return (
        str(item.get("delivery_class", "") or "").strip().lower() == "request"
        and str(item.get("request_contract_version", "") or "").strip()
        == _REQUEST_V2_REDUCED_VERSION
        and item.get("sender_recovery_required") is True
    )


def _annotate_request_recovery_message(message: dict[str, Any]) -> dict[str, Any]:
    item = dict(message or {})
    delivery_class = str(item.get("delivery_class", "") or "").strip().lower()
    sender_id = str(item.get("sender_id", "") or "").strip()
    sender_seal = str(item.get("sender_seal", "") or "").strip()
    sender_is_blinded = sender_id.startswith("sealed:") or sender_id.startswith("sender_token:")
    if delivery_class != "request" or not sender_is_blinded or not sender_seal.startswith("v3:"):
        return item
    if not str(item.get("request_contract_version", "") or "").strip():
        item["request_contract_version"] = _REQUEST_V2_REDUCED_VERSION
    item["sender_recovery_required"] = True
    state = str(item.get("sender_recovery_state", "") or "").strip().lower()
    if state not in _REQUEST_V2_RECOVERY_STATES:
        state = "pending"
    item["sender_recovery_state"] = state
    return item


def _annotate_request_recovery_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return [_annotate_request_recovery_message(message) for message in (messages or [])]


def _request_duplicate_authority_rank(message: dict[str, Any]) -> int:
    item = dict(message or {})
    if str(item.get("delivery_class", "") or "").strip().lower() != "request":
        return 0
    if _is_canonical_reduced_request_message(item):
        return 3
    sender_id = str(item.get("sender_id", "") or "").strip()
    if sender_id.startswith("sealed:") or sender_id.startswith("sender_token:"):
        return 1
    if sender_id:
        return 2
    return 0


def _request_duplicate_recovery_rank(message: dict[str, Any]) -> int:
    if not _is_canonical_reduced_request_message(message):
        return 0
    state = str(dict(message or {}).get("sender_recovery_state", "") or "").strip().lower()
    if state == "verified":
        return 2
    if state == "pending":
        return 1
    return 0


def _poll_duplicate_source_rank(source: str) -> int:
    normalized = str(source or "").strip().lower()
    if normalized == "relay":
        return 2
    if normalized == "reticulum":
        return 1
    return 0


def _should_replace_dm_poll_duplicate(
    existing: dict[str, Any],
    existing_source: str,
    candidate: dict[str, Any],
    candidate_source: str,
) -> bool:
    candidate_authority = _request_duplicate_authority_rank(candidate)
    existing_authority = _request_duplicate_authority_rank(existing)
    if candidate_authority != existing_authority:
        return candidate_authority > existing_authority

    candidate_recovery = _request_duplicate_recovery_rank(candidate)
    existing_recovery = _request_duplicate_recovery_rank(existing)
    if candidate_recovery != existing_recovery:
        return candidate_recovery > existing_recovery

    candidate_source_rank = _poll_duplicate_source_rank(candidate_source)
    existing_source_rank = _poll_duplicate_source_rank(existing_source)
    if candidate_source_rank != existing_source_rank:
        return candidate_source_rank > existing_source_rank

    try:
        candidate_ts = float(candidate.get("timestamp", 0) or 0)
    except Exception:
        candidate_ts = 0.0
    try:
        existing_ts = float(existing.get("timestamp", 0) or 0)
    except Exception:
        existing_ts = 0.0
    return candidate_ts > existing_ts


DM_POLL_BATCH_LIMIT = 8
"""Maximum messages returned per DM poll. Overflow stays queued for subsequent polls."""


def _merge_dm_poll_messages(
    relay_messages: list[dict[str, Any]],
    direct_messages: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    merged: list[dict[str, Any]] = []
    index_by_msg_id: dict[str, tuple[int, str]] = {}

    def add_messages(items: list[dict[str, Any]], source: str) -> None:
        for original in items or []:
            item = dict(original or {})
            msg_id = str(item.get("msg_id", "") or "").strip()
            if not msg_id:
                merged.append(item)
                continue
            existing = index_by_msg_id.get(msg_id)
            if existing is None:
                index_by_msg_id[msg_id] = (len(merged), source)
                merged.append(item)
                continue
            index, existing_source = existing
            if _should_replace_dm_poll_duplicate(merged[index], existing_source, item, source):
                merged[index] = item
                index_by_msg_id[msg_id] = (index, source)

    add_messages(relay_messages, "relay")
    add_messages(direct_messages, "reticulum")
    return sorted(merged, key=lambda item: float(item.get("timestamp", 0) or 0))


@app.post("/api/mesh/dm/poll")
@limiter.limit("30/minute")
@requires_signed_write(kind=SignedWriteKind.DM_POLL)
async def dm_poll_secure(request: Request):
    return await _dm_poll_secure_from_signed_request(request)

@app.get("/api/mesh/dm/poll")
@limiter.limit("30/minute")
async def dm_poll(
    request: Request,
    agent_id: str = "",
    agent_token: str = "",
    agent_token_prev: str = "",
    agent_tokens: str = "",
):
    """Pick up all pending DMs. Removes them from mailbox after retrieval."""
    exposure = metadata_exposure_for_request(
        request,
        authenticated=_scoped_view_authenticated(request, "mesh"),
    )
    if _secure_dm_enabled() and not _legacy_dm_get_allowed():
        if agent_id or agent_token or agent_token_prev or agent_tokens:
            record_legacy_dm_get(operation="poll", blocked=True)
        return dm_mailbox_response_view(
            {"ok": False, "detail": "Legacy GET polling is disabled in secure mode", "messages": [], "count": 0},
            exposure=exposure,
        )
    if not agent_id and not agent_token and not agent_token_prev and not agent_tokens:
        return dm_mailbox_response_view(
            {"ok": True, "messages": [], "count": 0},
            exposure=exposure,
            diagnostic={"source_counts": {"legacy": 0, "returned": 0}, "token_count": 0},
        )
    from services.mesh.mesh_dm_relay import dm_relay
    tokens: list[str] = []
    if agent_tokens:
        for token in agent_tokens.split(","):
            token = token.strip()
            if token:
                tokens.append(token)
    if agent_token:
        tokens.append(agent_token)
    if agent_token_prev and agent_token_prev != agent_token:
        tokens.append(agent_token_prev)
    # Deduplicate while preserving order
    seen = set()
    unique_tokens: list[str] = []
    for token in tokens:
        if token in seen:
            continue
        seen.add(token)
        unique_tokens.append(token)
    msgs: list[dict] = []
    has_more = False
    if unique_tokens:
        record_legacy_dm_get(operation="poll", blocked=False)
        for token in unique_tokens[:32]:
            batch, more = dm_relay.collect_legacy(agent_token=token, limit=DM_POLL_BATCH_LIMIT - len(msgs))
            msgs.extend(batch)
            if more:
                has_more = True
            if len(msgs) >= DM_POLL_BATCH_LIMIT:
                has_more = True
                msgs = msgs[:DM_POLL_BATCH_LIMIT]
                break
    return dm_mailbox_response_view(
        {"ok": True, "messages": msgs, "count": len(msgs), "has_more": has_more},
        exposure=exposure,
        diagnostic={
            "source_counts": {"legacy": len(msgs), "returned": len(msgs)},
            "token_count": len(unique_tokens),
        },
    )


def _coarsen_dm_count(n: int) -> int:
    """Reduce DM count precision to limit API-observable cardinality metadata."""
    if n <= 1:
        return n
    if n <= 5:
        return 5
    if n <= 20:
        return 20
    return 50


@app.post("/api/mesh/dm/count")
@limiter.limit("60/minute")
@requires_signed_write(kind=SignedWriteKind.DM_COUNT)
async def dm_count_secure(request: Request):
    return await _dm_count_secure_from_signed_request(request)

@app.get("/api/mesh/dm/count")
@limiter.limit("60/minute")
async def dm_count(
    request: Request,
    agent_id: str = "",
    agent_token: str = "",
    agent_token_prev: str = "",
    agent_tokens: str = "",
):
    """Unread DM count (for notification badge). Lightweight poll."""
    exposure = metadata_exposure_for_request(
        request,
        authenticated=_scoped_view_authenticated(request, "mesh"),
    )
    if _secure_dm_enabled() and not _legacy_dm_get_allowed():
        if agent_id or agent_token or agent_token_prev or agent_tokens:
            record_legacy_dm_get(operation="count", blocked=True)
        return dm_mailbox_response_view(
            {"ok": False, "detail": "Legacy GET count is disabled in secure mode", "count": 0},
            exposure=exposure,
        )
    if not agent_id and not agent_token and not agent_token_prev and not agent_tokens:
        return dm_mailbox_response_view(
            {"ok": True, "count": 0},
            exposure=exposure,
            diagnostic={"source_counts": {"legacy": 0, "exact_total": 0}, "token_count": 0},
        )
    from services.mesh.mesh_dm_relay import dm_relay
    tokens: list[str] = []
    if agent_tokens:
        for token in agent_tokens.split(","):
            token = token.strip()
            if token:
                tokens.append(token)
    if agent_token:
        tokens.append(agent_token)
    if agent_token_prev and agent_token_prev != agent_token:
        tokens.append(agent_token_prev)
    # Deduplicate while preserving order
    seen = set()
    unique_tokens: list[str] = []
    for token in tokens:
        if token in seen:
            continue
        seen.add(token)
        unique_tokens.append(token)
    if unique_tokens:
        record_legacy_dm_get(operation="count", blocked=False)
        total = 0
        for token in unique_tokens[:32]:
            total += dm_relay.count_legacy(agent_token=token)
        return dm_mailbox_response_view(
            {"ok": True, "count": _coarsen_dm_count(total)},
            exposure=exposure,
            diagnostic={"source_counts": {"legacy": total, "exact_total": total}, "token_count": len(unique_tokens)},
        )
    return dm_mailbox_response_view(
        {"ok": True, "count": 0},
        exposure=exposure,
        diagnostic={"source_counts": {"legacy": 0, "exact_total": 0}, "token_count": 0},
    )


@app.post("/api/mesh/dm/block")
@limiter.limit("10/minute")
@requires_signed_write(kind=SignedWriteKind.DM_BLOCK)
async def dm_block(request: Request):
    """Block or unblock a sender from DMing you."""
    body = _signed_body(request)
    agent_id = body.get("agent_id", "").strip()
    blocked_id = body.get("blocked_id", "").strip()
    action = body.get("action", "block").strip().lower()
    public_key = body.get("public_key", "").strip()
    public_key_algo = body.get("public_key_algo", "").strip()
    signature = body.get("signature", "").strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "").strip()
    if not agent_id or not blocked_id:
        return {"ok": False, "detail": "Missing agent_id or blocked_id"}
    from services.mesh.mesh_dm_relay import dm_relay

    try:
        from services.mesh.mesh_hashchain import infonet

        ok_seq, seq_reason = _validate_private_signed_sequence(
            infonet,
            agent_id,
            sequence,
            domain=f"dm_block:{action}",
        )
        if not ok_seq:
            return {"ok": False, "detail": seq_reason}
    except Exception:
        pass

    if action == "unblock":
        dm_relay.unblock(agent_id, blocked_id)
    else:
        dm_relay.block(agent_id, blocked_id)
    return {"ok": True, "action": action, "blocked_id": blocked_id}


@app.post("/api/mesh/dm/witness")
@limiter.limit("20/minute")
@requires_signed_write(kind=SignedWriteKind.DM_WITNESS)
async def dm_key_witness(request: Request):
    """Record a lightweight witness for a DM key (dual-path spot-check)."""
    body = _signed_body(request)
    witness_id = body.get("witness_id", "").strip()
    target_id = body.get("target_id", "").strip()
    dh_pub_key = body.get("dh_pub_key", "").strip()
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    public_key = body.get("public_key", "").strip()
    public_key_algo = body.get("public_key_algo", "").strip()
    signature = body.get("signature", "").strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "").strip()
    if not witness_id or not target_id or not dh_pub_key or not timestamp:
        return {"ok": False, "detail": "Missing witness_id, target_id, dh_pub_key, or timestamp"}
    now_ts = int(time.time())
    if abs(timestamp - now_ts) > 7 * 86400:
        return {"ok": False, "detail": "Witness timestamp is too far from current time"}

    try:
        from services.mesh.mesh_reputation import reputation_ledger

        reputation_ledger.register_node(witness_id, public_key, public_key_algo)
    except Exception:
        pass
    try:
        from services.mesh.mesh_hashchain import infonet

        ok_seq, seq_reason = _validate_private_signed_sequence(
            infonet,
            witness_id,
            sequence,
            domain="dm_witness",
        )
        if not ok_seq:
            return {"ok": False, "detail": seq_reason}
    except Exception:
        pass
    from services.mesh.mesh_dm_relay import dm_relay

    ok, reason = dm_relay.record_witness(witness_id, target_id, dh_pub_key, timestamp)
    return {"ok": ok, "detail": reason}


@app.get("/api/mesh/dm/witness")
@limiter.limit("60/minute")
async def dm_key_witness_get(request: Request, target_id: str = "", dh_pub_key: str = ""):
    """Get witness counts for a target's DH key."""
    if not target_id:
        return {"ok": False, "detail": "Missing target_id"}
    from services.mesh.mesh_dm_relay import dm_relay

    witnesses = dm_relay.get_witnesses(target_id, dh_pub_key if dh_pub_key else None, limit=5)
    response = {
        "ok": True,
        "count": len(witnesses),
    }
    if _scoped_view_authenticated(request, "mesh.audit"):
        response["target_id"] = target_id
        response["dh_pub_key"] = dh_pub_key or ""
        response["witnesses"] = witnesses
    return response


@app.post("/api/mesh/trust/vouch")
@limiter.limit("20/minute")
@requires_signed_write(kind=SignedWriteKind.TRUST_VOUCH)
async def trust_vouch(request: Request):
    """Record a trust vouch for a node (web-of-trust signal)."""
    body = _signed_body(request)
    voucher_id = body.get("voucher_id", "").strip()
    target_id = body.get("target_id", "").strip()
    note = body.get("note", "").strip()
    timestamp = _safe_int(body.get("timestamp", 0) or 0)
    public_key = body.get("public_key", "").strip()
    public_key_algo = body.get("public_key_algo", "").strip()
    signature = body.get("signature", "").strip()
    sequence = _safe_int(body.get("sequence", 0) or 0)
    protocol_version = body.get("protocol_version", "").strip()
    if not voucher_id or not target_id or not timestamp:
        return {"ok": False, "detail": "Missing voucher_id, target_id, or timestamp"}
    now_ts = int(time.time())
    if abs(timestamp - now_ts) > 7 * 86400:
        return {"ok": False, "detail": "Vouch timestamp is too far from current time"}
    try:
        from services.mesh.mesh_reputation import reputation_ledger
        from services.mesh.mesh_hashchain import infonet

        reputation_ledger.register_node(voucher_id, public_key, public_key_algo)
        ok_seq, seq_reason = _validate_private_signed_sequence(
            infonet,
            voucher_id,
            sequence,
            domain="trust_vouch",
        )
        if not ok_seq:
            return {"ok": False, "detail": seq_reason}
        ok, reason = reputation_ledger.add_vouch(voucher_id, target_id, note, timestamp)
        return {"ok": ok, "detail": reason}
    except Exception:
        return {"ok": False, "detail": "Failed to record vouch"}


@app.get("/api/mesh/trust/vouches", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def trust_vouches(request: Request, node_id: str = "", limit: int = 20):
    """Fetch latest vouches for a node."""
    if not node_id:
        return {"ok": False, "detail": "Missing node_id"}
    try:
        from services.mesh.mesh_reputation import reputation_ledger

        vouches = reputation_ledger.get_vouches(node_id, limit=limit)
        return {"ok": True, "node_id": node_id, "vouches": vouches, "count": len(vouches)}
    except Exception:
        return {"ok": False, "detail": "Failed to fetch vouches"}


@app.get("/api/debug-latest", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def debug_latest_data(request: Request):
    return list(get_latest_data().keys())


# ├óΓÇ¥Γé¼├óΓÇ¥Γé¼ CCTV media proxy (bypass CORS for cross-origin video/image streams) ├óΓÇ¥Γé¼├óΓÇ¥Γé¼├óΓÇ¥Γé¼
_CCTV_PROXY_ALLOWED_HOSTS = {
    "s3-eu-west-1.amazonaws.com",  # TfL JamCams
    "jamcams.tfl.gov.uk",
    "images.data.gov.sg",  # Singapore LTA
    "cctv.austinmobility.io",
    "webcams.nyctmc.org",
    # State DOT camera feeds often resolve to separate media/CDN hosts from the
    # catalog/API hostname. Keep the proxy allowlist aligned with the actual
    # media hosts produced by trusted ingestors so cameras render reliably.
    "cwwp2.dot.ca.gov",  # Caltrans
    "wzmedia.dot.ca.gov",  # Caltrans static media
    "images.wsdot.wa.gov",  # WSDOT
    "olypen.com",  # WSDOT Aviation-linked public camera
    "flyykm.com",  # WSDOT Aviation-linked public camera
    "cam.pangbornairport.com",  # WSDOT Aviation-linked public camera
    "navigator-c2c.dot.ga.gov",  # Georgia DOT
    "navigator-c2c.ga.gov",  # Georgia DOT alternate host variant
    "navigator-csc.dot.ga.gov",  # Georgia DOT alternate catalog/media host
    "vss1live.dot.ga.gov",  # Georgia DOT stream hosts
    "vss2live.dot.ga.gov",
    "vss3live.dot.ga.gov",
    "vss4live.dot.ga.gov",
    "vss5live.dot.ga.gov",
    "511ga.org",  # Georgia public camera images
    "gettingaroundillinois.com",  # Illinois DOT
    "cctv.travelmidwest.com",  # Illinois DOT camera media
    "mdotjboss.state.mi.us",  # Michigan DOT
    "micamerasimages.net",  # Michigan DOT image host
    "publicstreamer1.cotrip.org",  # Colorado DOT / COtrip HLS hosts
    "publicstreamer2.cotrip.org",
    "publicstreamer3.cotrip.org",
    "publicstreamer4.cotrip.org",
    "cocam.carsprogram.org",  # Colorado DOT preview images
    "tripcheck.com",  # Oregon DOT / TripCheck
    "www.tripcheck.com",
    "infocar.dgt.es",  # Spain DGT
    "etraffic.dgt.es",  # Spain DGT (etrafficWEB cameras host, 2026)
    "informo.madrid.es",  # Madrid
    "webcams2.asfinag.at",  # Austria ASFINAG motorway cameras
    "odo.asfinag.at",  # ASFINAG catalog API host
    "www.windy.com",
    "imgproxy.windy.com",  # Windy preview image CDN
    "www.lakecountypassage.com",  # Illinois Lake County PASSAGE snapshots
    "webcam.forkswa.com",  # WSDOT partner public camera
    "webcam.sunmountainlodge.com",  # WSDOT partner public camera
    "www.nps.gov",  # WSDOT-linked Mount Rainier camera
    "home.lewiscounty.com",  # WSDOT partner public camera
    "www.seattle.gov",  # Seattle traffic camera media linked from WSDOT
    "511on.ca",  # Ontario 511 cameras
    "511.alberta.ca",  # Alberta 511 cameras
    "fl511.com",  # Florida 511 cameras
    "www.fl511.com",
    "webcams.transport.nsw.gov.au",  # NSW Live Traffic camera snapshots
    "www.livetraffic.com",
    "livetraffic.com",
    "opendata.ndw.nu",  # Netherlands RWS legacy open-data host
}


@dataclass(frozen=True)
class _CCTVProxyProfile:
    name: str
    timeout: tuple[float, float] = (5.0, 10.0)
    cache_seconds: int = 30
    headers: dict[str, str] = field(default_factory=dict)


def _cctv_host_allowed(hostname: str | None) -> bool:
    host = str(hostname or "").strip().lower()
    if not host:
        return False
    for allowed in _CCTV_PROXY_ALLOWED_HOSTS:
        normalized = str(allowed or "").strip().lower()
        if host == normalized or host.endswith(f".{normalized}"):
            return True
    return False


def _proxied_cctv_url(target_url: str) -> str:
    from urllib.parse import quote

    return f"/api/cctv/media?url={quote(target_url, safe='')}"


def _cctv_proxy_profile_for_url(target_url: str) -> _CCTVProxyProfile:
    from urllib.parse import urlparse

    parsed = urlparse(target_url)
    host = str(parsed.hostname or "").strip().lower()
    path = str(parsed.path or "").strip().lower()

    if host in {"jamcams.tfl.gov.uk", "s3-eu-west-1.amazonaws.com"}:
        return _CCTVProxyProfile(
            name="tfl-jamcam",
            timeout=(5.0, 20.0),
            cache_seconds=15,
            headers={
                "Accept": "video/mp4,image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://tfl.gov.uk/",
            },
        )
    if host == "images.data.gov.sg":
        return _CCTVProxyProfile(
            name="lta-singapore",
            timeout=(5.0, 10.0),
            cache_seconds=30,
            headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
        )
    if host == "cctv.austinmobility.io":
        return _CCTVProxyProfile(
            name="austin-mobility",
            timeout=(5.0, 8.0),
            cache_seconds=15,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://data.mobility.austin.gov/",
                "Origin": "https://data.mobility.austin.gov",
            },
        )
    if host == "webcams.nyctmc.org":
        return _CCTVProxyProfile(
            name="nyc-dot",
            timeout=(5.0, 10.0),
            cache_seconds=15,
            headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
        )
    if host in {"cwwp2.dot.ca.gov", "wzmedia.dot.ca.gov"}:
        return _CCTVProxyProfile(
            name="caltrans",
            timeout=(5.0, 15.0),
            cache_seconds=15,
            headers={
                "Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,image/*,*/*;q=0.8",
                "Referer": "https://cwwp2.dot.ca.gov/",
            },
        )
    if host in {"images.wsdot.wa.gov", "olypen.com", "flyykm.com", "cam.pangbornairport.com"}:
        return _CCTVProxyProfile(
            name="wsdot",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
        )
    if host in {"navigator-c2c.dot.ga.gov", "navigator-c2c.ga.gov", "navigator-csc.dot.ga.gov"}:
        read_timeout = 18.0 if "/snapshots/" in path else 12.0
        return _CCTVProxyProfile(
            name="gdot-snapshot",
            timeout=(5.0, read_timeout),
            cache_seconds=15,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://navigator-c2c.dot.ga.gov/",
            },
        )
    if host == "511ga.org":
        return _CCTVProxyProfile(
            name="gdot-511ga-image",
            timeout=(5.0, 12.0),
            cache_seconds=15,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://511ga.org/cctv",
            },
        )
    if host.startswith("vss") and host.endswith("dot.ga.gov"):
        return _CCTVProxyProfile(
            name="gdot-hls",
            timeout=(5.0, 20.0),
            cache_seconds=10,
            headers={
                "Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8",
                "Referer": "https://navigator-c2c.dot.ga.gov/",
            },
        )
    if host in {"gettingaroundillinois.com", "cctv.travelmidwest.com"}:
        return _CCTVProxyProfile(
            name="illinois-dot",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
        )
    if host == "www.lakecountypassage.com":
        return _CCTVProxyProfile(
            name="lake-county-passage",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://www.lakecountypassage.com/",
            },
        )
    if host in {"mdotjboss.state.mi.us", "micamerasimages.net"}:
        return _CCTVProxyProfile(
            name="michigan-dot",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://mdotjboss.state.mi.us/",
            },
        )
    if host in {
        "publicstreamer1.cotrip.org",
        "publicstreamer2.cotrip.org",
        "publicstreamer3.cotrip.org",
        "publicstreamer4.cotrip.org",
    }:
        return _CCTVProxyProfile(
            name="cotrip-hls",
            timeout=(5.0, 20.0),
            cache_seconds=10,
            headers={
                "Accept": "application/vnd.apple.mpegurl,application/x-mpegURL,video/*,*/*;q=0.8",
                "Referer": "https://www.cotrip.org/",
            },
        )
    if host == "cocam.carsprogram.org":
        return _CCTVProxyProfile(
            name="cotrip-preview",
            timeout=(5.0, 12.0),
            cache_seconds=20,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://www.cotrip.org/",
            },
        )
    if host in {"tripcheck.com", "www.tripcheck.com"}:
        return _CCTVProxyProfile(
            name="odot-tripcheck",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
        )
    if host in {"infocar.dgt.es", "etraffic.dgt.es"}:
        return _CCTVProxyProfile(
            name="dgt-spain",
            timeout=(5.0, 8.0),
            cache_seconds=60,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://etraffic.dgt.es/",
            },
        )
    if host == "informo.madrid.es":
        return _CCTVProxyProfile(
            name="madrid-city",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://informo.madrid.es/",
            },
        )
    if host in {"webcams2.asfinag.at", "odo.asfinag.at"}:
        return _CCTVProxyProfile(
            name="asfinag-austria",
            timeout=(5.0, 15.0),
            cache_seconds=60,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://www.asfinag.at/",
            },
        )
    if host in {"www.windy.com", "imgproxy.windy.com"}:
        return _CCTVProxyProfile(
            name="windy-webcams",
            timeout=(5.0, 12.0),
            cache_seconds=60,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://www.windy.com/",
            },
        )
    if host == "511on.ca":
        return _CCTVProxyProfile(
            name="ontario-511",
            timeout=(5.0, 15.0),
            cache_seconds=30,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://511on.ca/",
            },
        )
    if host == "511.alberta.ca":
        return _CCTVProxyProfile(
            name="alberta-511",
            timeout=(5.0, 15.0),
            cache_seconds=30,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://511.alberta.ca/",
            },
        )
    if host in {"fl511.com", "www.fl511.com"}:
        return _CCTVProxyProfile(
            name="florida-511",
            timeout=(5.0, 15.0),
            cache_seconds=30,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://fl511.com/",
            },
        )
    if host == "webcams.transport.nsw.gov.au":
        return _CCTVProxyProfile(
            name="nsw-live-traffic",
            timeout=(5.0, 12.0),
            cache_seconds=60,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://www.livetraffic.com/",
            },
        )
    if host in {"opendata.ndw.nu", "www.ndw.nu"}:
        return _CCTVProxyProfile(
            name="ndw-netherlands",
            timeout=(5.0, 12.0),
            cache_seconds=120,
            headers={
                "Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
                "Referer": "https://www.ndw.nu/",
            },
        )
    if host in {
        "webcam.forkswa.com",
        "webcam.sunmountainlodge.com",
        "www.nps.gov",
        "home.lewiscounty.com",
        "www.seattle.gov",
    }:
        return _CCTVProxyProfile(
            name="wsdot-partner",
            timeout=(5.0, 12.0),
            cache_seconds=30,
            headers={"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"},
        )
    return _CCTVProxyProfile(
        name="generic-cctv",
        timeout=(5.0, 10.0),
        cache_seconds=30,
        headers={"Accept": "*/*"},
    )


def _cctv_upstream_headers(request: Request, profile: _CCTVProxyProfile) -> dict[str, str]:
    # Round 7a: per-install operator handle. See routers/cctv.py for the
    # canonical handler; this duplicate stays in lockstep until the #239
    # dedup ladder removes it.
    from services.network_utils import outbound_user_agent
    headers = {
        "User-Agent": f"Mozilla/5.0 (compatible; {outbound_user_agent('cctv-proxy')})",
        **profile.headers,
    }
    range_header = request.headers.get("range")
    if range_header:
        headers["Range"] = range_header
    if_none_match = request.headers.get("if-none-match")
    if if_none_match:
        headers["If-None-Match"] = if_none_match
    if_modified_since = request.headers.get("if-modified-since")
    if if_modified_since:
        headers["If-Modified-Since"] = if_modified_since
    return headers


def _cctv_response_headers(resp, cache_seconds: int, include_length: bool = True) -> dict[str, str]:
    headers = {
        "Cache-Control": f"public, max-age={cache_seconds}",
        "Access-Control-Allow-Origin": "*",
    }
    for key in ("Accept-Ranges", "Content-Range", "ETag", "Last-Modified"):
        value = resp.headers.get(key)
        if value:
            headers[key] = value
    if include_length:
        content_length = resp.headers.get("Content-Length")
        if content_length:
            headers["Content-Length"] = content_length
    return headers


def _infer_cctv_media_type_from_url(target_url: str, content_type: str) -> str:
    from urllib.parse import urlparse

    normalized_type = str(content_type or "").split(";", 1)[0].strip().lower()
    if normalized_type and normalized_type not in {"application/octet-stream", "binary/octet-stream"}:
        return content_type
    path = str(urlparse(target_url).path or "").lower()
    if path.endswith((".jpg", ".jpeg")):
        return "image/jpeg"
    if path.endswith(".png"):
        return "image/png"
    if path.endswith(".gif"):
        return "image/gif"
    if path.endswith(".webp"):
        return "image/webp"
    if path.endswith(".mp4"):
        return "video/mp4"
    if path.endswith(".webm"):
        return "video/webm"
    if path.endswith(".m3u8"):
        return "application/vnd.apple.mpegurl"
    return content_type or "application/octet-stream"


def _fetch_cctv_upstream_response(request: Request, target_url: str, profile: _CCTVProxyProfile):
    import requests as _req

    headers = _cctv_upstream_headers(request, profile)
    try:
        resp = _req.get(
            target_url,
            timeout=profile.timeout,
            stream=True,
            allow_redirects=True,
            headers=headers,
        )
    except _req.exceptions.Timeout as exc:
        logger.warning("CCTV upstream timeout [%s] %s", profile.name, target_url)
        raise HTTPException(status_code=504, detail="Upstream timeout") from exc
    except _req.exceptions.RequestException as exc:
        logger.warning("CCTV upstream request failure [%s] %s: %s", profile.name, target_url, exc)
        raise HTTPException(status_code=502, detail="Upstream fetch failed") from exc

    if resp.status_code >= 400:
        logger.info("CCTV upstream HTTP %s [%s] %s", resp.status_code, profile.name, target_url)
        resp.close()
        raise HTTPException(status_code=int(resp.status_code), detail=f"Upstream returned {resp.status_code}")
    return resp


def _proxy_cctv_media_response(request: Request, target_url: str):
    from urllib.parse import urlparse

    parsed = urlparse(target_url)
    profile = _cctv_proxy_profile_for_url(target_url)
    resp = _fetch_cctv_upstream_response(request, target_url, profile)

    content_type = _infer_cctv_media_type_from_url(
        target_url,
        resp.headers.get("Content-Type", "application/octet-stream"),
    )
    is_hls_playlist = (
        ".m3u8" in str(parsed.path or "").lower()
        or "mpegurl" in content_type.lower()
        or "vnd.apple.mpegurl" in content_type.lower()
    )
    if is_hls_playlist:
        body = resp.text
        if "#EXTM3U" in body:
            body = _rewrite_cctv_hls_playlist(target_url, body)
        resp.close()
        return Response(
            content=body,
            media_type=content_type,
            headers=_cctv_response_headers(resp, cache_seconds=profile.cache_seconds, include_length=False),
        )
    return StreamingResponse(
        resp.iter_content(chunk_size=65536),
        status_code=resp.status_code,
        media_type=content_type,
        headers=_cctv_response_headers(resp, cache_seconds=profile.cache_seconds),
        background=BackgroundTask(resp.close),
    )


def _rewrite_cctv_hls_playlist(base_url: str, body: str) -> str:
    import re
    from urllib.parse import urljoin, urlparse

    def _rewrite_target(target: str) -> str:
        candidate = str(target or "").strip()
        if not candidate or candidate.startswith("data:"):
            return candidate
        absolute = urljoin(base_url, candidate)
        parsed_target = urlparse(absolute)
        if parsed_target.scheme not in ("http", "https"):
            return candidate
        if not _cctv_host_allowed(parsed_target.hostname):
            return candidate
        return _proxied_cctv_url(absolute)

    rewritten_lines: list[str] = []
    for raw_line in body.splitlines():
        stripped = raw_line.strip()
        if not stripped:
            rewritten_lines.append(raw_line)
            continue
        if stripped.startswith("#"):
            rewritten_lines.append(
                re.sub(
                    r'URI="([^"]+)"',
                    lambda match: f'URI="{_rewrite_target(match.group(1))}"',
                    raw_line,
                )
            )
            continue
        rewritten_lines.append(_rewrite_target(stripped))
    return "\n".join(rewritten_lines) + ("\n" if body.endswith("\n") else "")


@app.get("/api/cctv/media")
@limiter.limit("120/minute")
async def cctv_media_proxy(request: Request, url: str = Query(...)):
    """Proxy CCTV media through the backend to bypass browser CORS restrictions."""
    from urllib.parse import urlparse

    parsed = urlparse(url)
    if not _cctv_host_allowed(parsed.hostname):
        raise HTTPException(status_code=403, detail="Host not allowed")
    if parsed.scheme not in ("http", "https"):
        raise HTTPException(status_code=400, detail="Invalid scheme")

    return _proxy_cctv_media_response(request, url)


@app.get("/api/health", response_model=HealthResponse)
@limiter.limit("30/minute")
async def health_check(request: Request):
    import time
    from services.fetchers._store import get_source_timestamps_snapshot

    d = get_latest_data()
    last = d.get("last_updated")
    return {
        "status": "ok",
        "version": APP_VERSION,
        "last_updated": last,
        "sources": {
            "flights": len(d.get("commercial_flights", [])),
            "military": len(d.get("military_flights", [])),
            "ships": len(d.get("ships", [])),
            "satellites": len(d.get("satellites", [])),
            "earthquakes": len(d.get("earthquakes", [])),
            "cctv": len(d.get("cctv", [])),
            "news": len(d.get("news", [])),
            "uavs": len(d.get("uavs", [])),
            "firms_fires": len(d.get("firms_fires", [])),
            "liveuamap": len(d.get("liveuamap", [])),
            "gdelt": len(d.get("gdelt", [])),
        },
        "freshness": get_source_timestamps_snapshot(),
        "uptime_seconds": round(time.time() - _start_time),
    }


from services.radio_intercept import (
    get_top_broadcastify_feeds,
    get_openmhz_systems,
    get_recent_openmhz_calls,
    find_nearest_openmhz_system,
)


@app.get("/api/radio/top")
@limiter.limit("30/minute")
async def get_top_radios(request: Request):
    return get_top_broadcastify_feeds()


@app.get("/api/radio/openmhz/systems")
@limiter.limit("30/minute")
async def api_get_openmhz_systems(request: Request):
    return get_openmhz_systems()


@app.get("/api/radio/openmhz/calls/{sys_name}")
@limiter.limit("60/minute")
async def api_get_openmhz_calls(request: Request, sys_name: str):
    return get_recent_openmhz_calls(sys_name)


@app.get("/api/radio/openmhz/audio")
@limiter.limit("120/minute")
async def api_get_openmhz_audio(request: Request, url: str = Query(..., min_length=10)):
    from services.radio_intercept import openmhz_audio_response
    return openmhz_audio_response(url)


@app.get("/api/radio/nearest")
@limiter.limit("60/minute")
async def api_get_nearest_radio(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
):
    return find_nearest_openmhz_system(lat, lng)


from services.radio_intercept import find_nearest_openmhz_systems_list


@app.get("/api/radio/nearest-list")
@limiter.limit("60/minute")
async def api_get_nearest_radios_list(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
    limit: int = Query(5, ge=1, le=20),
):
    return find_nearest_openmhz_systems_list(lat, lng, limit=limit)


from services.network_utils import fetch_with_curl


@app.get("/api/route/{callsign}")
@limiter.limit("60/minute")
async def get_flight_route(request: Request, callsign: str, lat: float = 0.0, lng: float = 0.0):
    r = fetch_with_curl(
        "https://api.adsb.lol/api/0/routeset",
        method="POST",
        json_data={"planes": [{"callsign": callsign, "lat": lat, "lng": lng}]},
        timeout=10,
    )
    if r and r.status_code == 200:
        data = r.json()
        route_list = []
        if isinstance(data, dict):
            route_list = data.get("value", [])
        elif isinstance(data, list):
            route_list = data

        if route_list and len(route_list) > 0:
            route = route_list[0]
            airports = route.get("_airports", [])
            if len(airports) >= 2:
                orig = airports[0]
                dest = airports[-1]
                return {
                    "orig_loc": [orig.get("lon", 0), orig.get("lat", 0)],
                    "dest_loc": [dest.get("lon", 0), dest.get("lat", 0)],
                    "origin_name": f"{orig.get('iata', '') or orig.get('icao', '')}: {orig.get('name', 'Unknown')}",
                    "dest_name": f"{dest.get('iata', '') or dest.get('icao', '')}: {dest.get('name', 'Unknown')}",
                }
    return {}


from services.region_dossier import get_region_dossier


@app.get("/api/region-dossier")
@limiter.limit("30/minute")
def api_region_dossier(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
):
    """Sync def so FastAPI runs it in a threadpool ├óΓé¼ΓÇ¥ prevents blocking the event loop."""
    return get_region_dossier(lat, lng)


# ---------------------------------------------------------------------------
# Geocoding ├óΓé¼ΓÇ¥ proxy to Nominatim with caching and proper headers
# ---------------------------------------------------------------------------
from services.geocode import search_geocode, reverse_geocode


@app.get("/api/geocode/search")
@limiter.limit("30/minute")
async def api_geocode_search(
    request: Request,
    q: str = "",
    limit: int = 5,
    local_only: bool = False,
):
    if not q or len(q.strip()) < 2:
        return {"results": [], "query": q, "count": 0}
    results = await asyncio.to_thread(search_geocode, q, limit, local_only)
    return {"results": results, "query": q, "count": len(results)}


@app.get("/api/geocode/reverse")
@limiter.limit("60/minute")
async def api_geocode_reverse(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
    local_only: bool = False,
):
    return await asyncio.to_thread(reverse_geocode, lat, lng, local_only)


from services.sentinel_search import search_sentinel2_scene


@app.get("/api/sentinel2/search")
@limiter.limit("30/minute")
def api_sentinel2_search(
    request: Request,
    lat: float = Query(..., ge=-90, le=90),
    lng: float = Query(..., ge=-180, le=180),
):
    """Search for latest Sentinel-2 imagery at a point. Sync for threadpool execution."""
    return search_sentinel2_scene(lat, lng)


@app.post("/api/sentinel/token")
@limiter.limit("60/minute")
async def api_sentinel_token(request: Request):
    """Proxy Copernicus CDSE OAuth2 token request (avoids browser CORS block).

    The user's client_id + client_secret are forwarded to the Copernicus
    identity provider and never stored on the server.
    """
    import requests as req

    # Parse URL-encoded form body manually (avoids python-multipart dependency)
    body = await request.body()
    from urllib.parse import parse_qs
    params = parse_qs(body.decode("utf-8"))
    client_id = params.get("client_id", [""])[0]
    client_secret = params.get("client_secret", [""])[0]

    if not client_id or not client_secret:
        raise HTTPException(400, "client_id and client_secret required")

    token_url = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
    try:
        resp = await asyncio.to_thread(
            req.post,
            token_url,
            data={
                "grant_type": "client_credentials",
                "client_id": client_id,
                "client_secret": client_secret,
            },
            timeout=15,
        )
        return Response(
            content=resp.content,
            status_code=resp.status_code,
            media_type="application/json",
        )
    except Exception as exc:
        logger.exception("Token request failed")
        raise HTTPException(502, "Token request failed")


# Server-side token cache for tile requests (avoids re-auth on every tile)
_sh_token_cache: dict = {"token": None, "expiry": 0, "client_id": ""}


@app.post("/api/sentinel/tile")
@limiter.limit("300/minute")
async def api_sentinel_tile(request: Request):
    """Proxy Sentinel Hub Process API tile request (avoids CORS block).

    Expects JSON body with: client_id, client_secret, preset, date, z, x, y.
    Returns the PNG tile directly.
    """
    import requests as req
    import time as _time

    try:
        body = await request.json()
    except Exception:
        return JSONResponse(status_code=422, content={"ok": False, "detail": "invalid JSON body"})

    client_id = body.get("client_id", "")
    client_secret = body.get("client_secret", "")
    preset = body.get("preset", "TRUE-COLOR")
    date_str = body.get("date", "")
    z = body.get("z", 0)
    x = body.get("x", 0)
    y = body.get("y", 0)

    if not client_id or not client_secret or not date_str:
        raise HTTPException(400, "client_id, client_secret, and date required")

    # Reuse cached token if same client_id and not expired
    now = _time.time()
    if (
        _sh_token_cache["token"]
        and _sh_token_cache["client_id"] == client_id
        and now < _sh_token_cache["expiry"] - 30
    ):
        token = _sh_token_cache["token"]
    else:
        # Fetch new token
        token_url = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
        try:
            tresp = await asyncio.to_thread(
                req.post,
                token_url,
                data={
                    "grant_type": "client_credentials",
                    "client_id": client_id,
                    "client_secret": client_secret,
                },
                timeout=15,
            )
            if tresp.status_code != 200:
                raise HTTPException(401, f"Token auth failed: {tresp.text[:200]}")
            tdata = tresp.json()
            token = tdata["access_token"]
            _sh_token_cache["token"] = token
            _sh_token_cache["expiry"] = now + tdata.get("expires_in", 300)
            _sh_token_cache["client_id"] = client_id
        except HTTPException:
            raise
        except Exception as exc:
            logger.exception("Token request failed")
            raise HTTPException(502, "Token request failed")

    # Compute bounding box from tile coordinates (EPSG:3857)
    import math

    half = 20037508.342789244
    tile_size = (2 * half) / math.pow(2, z)
    min_x = -half + x * tile_size
    max_x = min_x + tile_size
    max_y = half - y * tile_size
    min_y = max_y - tile_size
    bbox = [min_x, min_y, max_x, max_y]

    # Evalscripts
    evalscripts = {
        "TRUE-COLOR": '//VERSION=3\nfunction setup(){return{input:["B04","B03","B02"],output:{bands:3}};}\nfunction evaluatePixel(s){return[2.5*s.B04,2.5*s.B03,2.5*s.B02];}',
        "FALSE-COLOR": '//VERSION=3\nfunction setup(){return{input:["B08","B04","B03"],output:{bands:3}};}\nfunction evaluatePixel(s){return[2.5*s.B08,2.5*s.B04,2.5*s.B03];}',
        "NDVI": '//VERSION=3\nfunction setup(){return{input:["B04","B08"],output:{bands:3}};}\nfunction evaluatePixel(s){var n=(s.B08-s.B04)/(s.B08+s.B04);if(n<-0.2)return[0.05,0.05,0.05];if(n<0)return[0.75,0.75,0.75];if(n<0.1)return[0.86,0.86,0.86];if(n<0.2)return[0.92,0.84,0.68];if(n<0.3)return[0.77,0.88,0.55];if(n<0.4)return[0.56,0.80,0.32];if(n<0.5)return[0.35,0.72,0.18];if(n<0.6)return[0.20,0.60,0.08];if(n<0.7)return[0.10,0.48,0.04];return[0.0,0.36,0.0];}',
        "MOISTURE-INDEX": '//VERSION=3\nfunction setup(){return{input:["B8A","B11"],output:{bands:3}};}\nfunction evaluatePixel(s){var m=(s.B8A-s.B11)/(s.B8A+s.B11);var r=Math.max(0,Math.min(1,1.5-3*m));var g=Math.max(0,Math.min(1,m<0?1.5+3*m:1.5-3*m));var b=Math.max(0,Math.min(1,1.5+3*(m-0.5)));return[r,g,b];}',
    }
    evalscript = evalscripts.get(preset, evalscripts["TRUE-COLOR"])

    # Adaptive time range: wider window at lower zoom for better coverage.
    # Sentinel-2 has 5-day revisit ├óΓé¼ΓÇ¥ a single day often has gaps.
    # At low zoom we mosaic over more days to fill gaps.
    from datetime import datetime as _dt, timedelta as _td

    try:
        end_date = _dt.strptime(date_str, "%Y-%m-%d")
    except ValueError:
        end_date = _dt.utcnow()

    if z <= 6:
        lookback_days = 30  # continent-level: mosaic a full month
    elif z <= 9:
        lookback_days = 14  # region-level: 2 weeks
    elif z <= 11:
        lookback_days = 7   # country-level: 1 week
    else:
        lookback_days = 5   # close-up: 5 days (one revisit cycle)

    start_date = end_date - _td(days=lookback_days)

    process_body = {
        "input": {
            "bounds": {
                "bbox": bbox,
                "properties": {"crs": "http://www.opengis.net/def/crs/EPSG/0/3857"},
            },
            "data": [
                {
                    "type": "sentinel-2-l2a",
                    "dataFilter": {
                        "timeRange": {
                            "from": start_date.strftime("%Y-%m-%dT00:00:00Z"),
                            "to": end_date.strftime("%Y-%m-%dT23:59:59Z"),
                        },
                        "maxCloudCoverage": 30,
                        "mosaickingOrder": "leastCC",
                    },
                }
            ],
        },
        "output": {
            "width": 256,
            "height": 256,
            "responses": [{"identifier": "default", "format": {"type": "image/png"}}],
        },
        "evalscript": evalscript,
    }

    try:
        resp = await asyncio.to_thread(
            req.post,
            "https://sh.dataspace.copernicus.eu/api/v1/process",
            json=process_body,
            headers={
                "Authorization": f"Bearer {token}",
                "Accept": "image/png",
            },
            timeout=30,
        )
        return Response(
            content=resp.content,
            status_code=resp.status_code,
            media_type=resp.headers.get("content-type", "image/png"),
        )
    except Exception as exc:
        logger.exception("Process API failed")
        raise HTTPException(502, "Process API failed")


# ---------------------------------------------------------------------------
# API Settings ├óΓé¼ΓÇ¥ key registry & management
# ---------------------------------------------------------------------------
from services.api_settings import get_api_keys, get_env_path_info
from services.shodan_connector import (
    ShodanConnectorError,
    count_shodan,
    get_shodan_connector_status,
    lookup_shodan_host,
    search_shodan,
)
from pydantic import BaseModel


class ShodanSearchRequest(BaseModel):
    query: str
    page: int = 1
    facets: list[str] = []


class ShodanCountRequest(BaseModel):
    query: str
    facets: list[str] = []


class ShodanHostRequest(BaseModel):
    ip: str
    history: bool = False


@app.get("/api/settings/api-keys", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_get_keys(request: Request):
    return get_api_keys()


@app.get("/api/settings/api-keys/meta")
@limiter.limit("30/minute")
async def api_get_keys_meta(request: Request):
    return get_env_path_info()


@app.get("/api/tools/shodan/status", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_shodan_status(request: Request):
    return get_shodan_connector_status()


@app.post("/api/tools/shodan/search", dependencies=[Depends(require_local_operator)])
@limiter.limit("12/minute")
async def api_shodan_search(request: Request, body: ShodanSearchRequest):
    try:
        return search_shodan(body.query, page=body.page, facets=body.facets)
    except ShodanConnectorError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc


@app.post("/api/tools/shodan/count", dependencies=[Depends(require_local_operator)])
@limiter.limit("12/minute")
async def api_shodan_count(request: Request, body: ShodanCountRequest):
    try:
        return count_shodan(body.query, facets=body.facets)
    except ShodanConnectorError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc


@app.post("/api/tools/shodan/host", dependencies=[Depends(require_local_operator)])
@limiter.limit("12/minute")
async def api_shodan_host(request: Request, body: ShodanHostRequest):
    try:
        return lookup_shodan_host(body.ip, history=body.history)
    except ShodanConnectorError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc


# ---------------------------------------------------------------------------
# Finnhub ├óΓé¼ΓÇ¥ free market intelligence (quotes, congress trades, insider txns)
# ---------------------------------------------------------------------------
from services.unusual_whales_connector import (
    FinnhubConnectorError,
    get_uw_status,
    fetch_congress_trades,
    fetch_insider_transactions,
    fetch_defense_quotes,
)


@app.get("/api/tools/uw/status", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_uw_status(request: Request):
    return get_uw_status()


@app.post("/api/tools/uw/congress", dependencies=[Depends(require_local_operator)])
@limiter.limit("12/minute")
async def api_uw_congress(request: Request):
    try:
        return fetch_congress_trades()
    except FinnhubConnectorError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc


@app.post("/api/tools/uw/darkpool", dependencies=[Depends(require_local_operator)])
@limiter.limit("12/minute")
async def api_uw_darkpool(request: Request):
    try:
        return fetch_insider_transactions()
    except FinnhubConnectorError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc


@app.post("/api/tools/uw/flow", dependencies=[Depends(require_local_operator)])
@limiter.limit("12/minute")
async def api_uw_flow(request: Request):
    try:
        return fetch_defense_quotes()
    except FinnhubConnectorError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc


# ---------------------------------------------------------------------------
# News Feed Configuration
# ---------------------------------------------------------------------------
from services.news_feed_config import get_feeds, save_feeds, reset_feeds


@app.get(
    "/api/settings/news-feeds",
    dependencies=[Depends(require_local_operator)],
)
@limiter.limit("30/minute")
async def api_get_news_feeds(request: Request):
    """Issue #252 (tg12): gated on local-operator. See the canonical
    handler in backend/routers/admin.py for the full rationale."""
    return get_feeds()


@app.put("/api/settings/news-feeds", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
async def api_save_news_feeds(request: Request):
    body = await request.json()
    ok = save_feeds(body)
    if ok:
        return {"status": "updated", "count": len(body)}
    return Response(
        content=json_mod.dumps(
            {
                "status": "error",
                "message": "Validation failed (max 20 feeds, each needs name/url/weight 1-5)",
            }
        ),
        status_code=400,
        media_type="application/json",
    )


@app.post("/api/settings/news-feeds/reset", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
async def api_reset_news_feeds(request: Request):
    ok = reset_feeds()
    if ok:
        return {"status": "reset", "feeds": get_feeds()}
    return {"status": "error", "message": "Failed to reset feeds"}


# ---------------------------------------------------------------------------
# Wormhole Settings ├óΓé¼ΓÇ¥ local agent toggle
# ---------------------------------------------------------------------------
from services.wormhole_settings import read_wormhole_settings, write_wormhole_settings
from services.wormhole_status import read_wormhole_status
from services.wormhole_supervisor import (
    connect_wormhole,
    disconnect_wormhole,
    get_wormhole_state,
    restart_wormhole,
)
from services.mesh import mesh_wormhole_identity as _mesh_wormhole_identity

bootstrap_wormhole_identity = _mesh_wormhole_identity.bootstrap_wormhole_identity
read_wormhole_identity = _mesh_wormhole_identity.read_wormhole_identity
register_wormhole_dm_key = _mesh_wormhole_identity.register_wormhole_dm_key
sign_wormhole_message = _mesh_wormhole_identity.sign_wormhole_message
sign_wormhole_event = _mesh_wormhole_identity.sign_wormhole_event


def _wormhole_identity_unavailable(*_args, **_kwargs) -> dict[str, Any]:
    return {"ok": False, "detail": "wormhole_identity_unavailable"}


export_wormhole_dm_invite = getattr(
    _mesh_wormhole_identity,
    "export_wormhole_dm_invite",
    _wormhole_identity_unavailable,
)
list_prekey_lookup_handle_records_for_ui = getattr(
    _mesh_wormhole_identity,
    "list_prekey_lookup_handle_records_for_ui",
    _wormhole_identity_unavailable,
)
revoke_prekey_lookup_handle = getattr(
    _mesh_wormhole_identity,
    "revoke_prekey_lookup_handle",
    _wormhole_identity_unavailable,
)
import_wormhole_dm_invite = getattr(
    _mesh_wormhole_identity,
    "import_wormhole_dm_invite",
    _wormhole_identity_unavailable,
)
lookup_handle_rotation_status_snapshot = getattr(
    _mesh_wormhole_identity,
    "lookup_handle_rotation_status_snapshot",
    lambda: {
        "state": "lookup_handle_rotation_unavailable",
        "detail": "wormhole_identity_unavailable",
        "active_handle_count": 0,
        "fresh_handle_available": False,
    },
)
maybe_rotate_prekey_lookup_handles = getattr(
    _mesh_wormhole_identity,
    "maybe_rotate_prekey_lookup_handles",
    lambda **_kwargs: {
        "ok": False,
        "rotated": False,
        "detail": "wormhole_identity_unavailable",
    },
)
from services.mesh.mesh_wormhole_persona import (
    activate_gate_persona,
    bootstrap_wormhole_persona_state,
    clear_active_gate_persona,
    create_gate_persona,
    enter_gate_anonymously,
    get_active_gate_identity,
    get_dm_identity,
    get_transport_identity,
    leave_gate,
    list_gate_personas,
    retire_gate_persona,
    sign_gate_wormhole_event,
    sign_public_wormhole_event,
)
from services.mesh import mesh_wormhole_prekey as _mesh_wormhole_prekey

bootstrap_decrypt_from_sender = _mesh_wormhole_prekey.bootstrap_decrypt_from_sender
bootstrap_encrypt_for_peer = _mesh_wormhole_prekey.bootstrap_encrypt_for_peer
fetch_dm_prekey_bundle = _mesh_wormhole_prekey.fetch_dm_prekey_bundle
register_wormhole_prekey_bundle = _mesh_wormhole_prekey.register_wormhole_prekey_bundle
observe_remote_prekey_bundle = getattr(
    _mesh_wormhole_prekey,
    "observe_remote_prekey_bundle",
    lambda *_args, **_kwargs: {
        "ok": False,
        "detail": "wormhole_prekey_unavailable",
    },
)
from services.mesh.mesh_wormhole_sender_token import (
    consume_wormhole_dm_sender_token,
    issue_wormhole_dm_sender_token,
    issue_wormhole_dm_sender_tokens,
)
from services.mesh.mesh_wormhole_seal import build_sender_seal, open_sender_seal
from services.mesh.mesh_wormhole_dead_drop import (
    AliasRotationReason,
    apply_inbound_alias_binding_frame,
    derive_dead_drop_token_pair,
    derive_dead_drop_tokens_for_contacts,
    derive_sas_phrase,
    issue_pairwise_dm_alias,
    mark_contact_alias_reply_observed,
    maybe_prepare_pairwise_dm_alias_rotation,
    PAIRWISE_ALIAS_GRACE_DEFAULT_MS,
    prepare_outbound_alias_binding_payload,
    register_outbound_alias_rotation_commit,
    rotate_pairwise_dm_alias,
    _unwrap_pairwise_alias_payload,
)
from services.mesh.mesh_gate_mls import (
    compose_encrypted_gate_message,
    decrypt_gate_message_for_local_identity,
    ensure_gate_member_access,
    export_gate_state_snapshot,
    get_local_gate_key_status,
    is_gate_locked_to_mls as is_gate_mls_locked,
    mark_gate_rekey_recommended,
    rotate_gate_epoch,
    sign_encrypted_gate_message,
)
try:
    from services.mesh.mesh_gate_repair import (
        compose_gate_message_with_repair,
        decrypt_gate_message_with_repair,
        export_gate_state_snapshot_with_repair,
        gate_repair_status_snapshot,
        sign_gate_message_with_repair,
    )
except Exception:
    compose_gate_message_with_repair = compose_encrypted_gate_message
    decrypt_gate_message_with_repair = decrypt_gate_message_for_local_identity
    export_gate_state_snapshot_with_repair = export_gate_state_snapshot
    sign_gate_message_with_repair = sign_encrypted_gate_message
    gate_repair_status_snapshot = lambda *_args, **_kwargs: {
        "available": False,
        "state": "gate_repair_unavailable",
    }
from services.mesh.mesh_dm_mls import (
    decrypt_dm as decrypt_mls_dm,
    encrypt_dm as encrypt_mls_dm,
    ensure_dm_session as ensure_mls_dm_session,
    has_dm_session as has_mls_dm_session,
    initiate_dm_session as initiate_mls_dm_session,
    is_dm_locked_to_mls,
)
from services.mesh.mesh_wormhole_ratchet import (
    decrypt_wormhole_dm,
    encrypt_wormhole_dm,
    reset_wormhole_dm_ratchet,
)


class WormholeUpdate(BaseModel):
    enabled: bool
    transport: str | None = None
    socks_proxy: str | None = None
    socks_dns: bool | None = None
    anonymous_mode: bool | None = None


class NodeSettingsUpdate(BaseModel):
    enabled: bool


@app.get("/api/settings/node")
@limiter.limit("30/minute")
async def api_get_node_settings(request: Request):
    """Issue #243 (tg12): node mode and participant state are
    operational posture. Anonymous callers receive an empty stub ΓÇö
    enough for the UI to know the endpoint exists but nothing
    fingerprintable. Authenticated callers see the full state.

    Authenticated == local-operator (loopback / Docker bridge) OR an
    admin / scoped-view token. The Tauri shell and Docker frontend
    container both qualify via their existing transport (PR #263 +
    PR #278), so legitimate operator UX is unchanged.
    """
    from services.node_settings import read_node_settings

    data = await asyncio.to_thread(read_node_settings)
    authenticated = _scoped_view_authenticated(request, "node")
    if not authenticated:
        return {}
    return {
        **data,
        "node_mode": _current_node_mode(),
        "node_enabled": _participant_node_enabled(),
    }


@app.put("/api/settings/node", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
async def api_set_node_settings(request: Request, body: NodeSettingsUpdate):
    _refresh_node_peer_store()
    if bool(body.enabled):
        if _infonet_private_transport_required() and not _ensure_infonet_private_transport_ready("operator_enable"):
            return JSONResponse(
                {"ok": False, "detail": _infonet_private_transport_error()},
                status_code=503,
            )
        try:
            from services.transport_lane_isolation import disable_public_mesh_lane

            disable_public_mesh_lane(reason="private_node_enabled")
        except Exception as exc:
            logger.warning("Failed to disable public Mesh while enabling private node: %s", exc)
    result = _set_participant_node_enabled(bool(body.enabled))
    if bool(body.enabled):
        _start_infonet_node_runtime("operator_enable")
        _kick_public_sync_background("operator_enable")
        threading.Thread(target=_swarm_bootstrap_after_transport_ready, daemon=True).start()
    return result


@app.post("/api/mesh/infonet/swarm/join")
@limiter.limit("10/minute")
async def infonet_swarm_join(request: Request):
    """Announce this node to the fleet seed and pull the signed peer manifest."""
    if not _participant_node_enabled():
        return {"ok": False, "detail": "participant node is disabled"}
    if _infonet_private_transport_required() and not _ensure_infonet_private_transport_ready("swarm_join"):
        return JSONResponse(
            {"ok": False, "detail": _infonet_private_transport_error()},
            status_code=503,
        )

    from services.mesh.mesh_swarm_runtime import announce_local_peer_to_seeds, refresh_swarm_manifest_from_seeds

    announce = await asyncio.to_thread(announce_local_peer_to_seeds, force=True)
    manifest = await asyncio.to_thread(refresh_swarm_manifest_from_seeds, force=True)
    if manifest.get("ok"):
        await asyncio.to_thread(_refresh_node_peer_store)
    return {
        "ok": bool(announce.get("ok")) or bool(manifest.get("ok")),
        "announce": announce,
        "manifest_pull": manifest,
    }


@app.get("/api/settings/wormhole")
@limiter.limit("30/minute")
async def api_get_wormhole_settings(request: Request):
    settings = await asyncio.to_thread(read_wormhole_settings)
    return _redact_wormhole_settings(settings, authenticated=_scoped_view_authenticated(request, "wormhole"))


@app.put("/api/settings/wormhole", dependencies=[Depends(require_admin)])
@limiter.limit("5/minute")
async def api_set_wormhole_settings(request: Request, body: WormholeUpdate):
    existing = read_wormhole_settings()
    updated = write_wormhole_settings(
        enabled=bool(body.enabled),
        transport=body.transport,
        socks_proxy=body.socks_proxy,
        socks_dns=body.socks_dns,
        anonymous_mode=body.anonymous_mode,
    )
    transport_changed = (
        str(existing.get("transport", "direct")) != str(updated.get("transport", "direct"))
        or str(existing.get("socks_proxy", "")) != str(updated.get("socks_proxy", ""))
        or bool(existing.get("socks_dns", True)) != bool(updated.get("socks_dns", True))
    )
    if bool(updated.get("enabled")):
        state = restart_wormhole(reason="settings_update") if transport_changed else connect_wormhole(reason="settings_enable")
    else:
        state = disconnect_wormhole(reason="settings_disable")
    return {**updated, "requires_restart": False, "runtime": state}


class PrivacyProfileUpdate(BaseModel):
    profile: str


class WormholeSignRequest(BaseModel):
    event_type: str
    payload: dict
    sequence: int | None = None
    gate_id: str | None = None


class WormholeSignRawRequest(BaseModel):
    message: str


class WormholeDmEncryptRequest(BaseModel):
    peer_id: str
    peer_dh_pub: str = ""
    plaintext: str
    local_alias: str | None = None
    remote_alias: str | None = None
    remote_prekey_bundle: dict[str, Any] | None = None


class WormholeDmComposeRequest(BaseModel):
    peer_id: str
    peer_dh_pub: str = ""
    plaintext: str
    local_alias: str | None = None
    remote_alias: str | None = None
    remote_prekey_bundle: dict[str, Any] | None = None


class WormholeDmDecryptRequest(BaseModel):
    peer_id: str
    ciphertext: str
    format: str = "dm1"
    nonce: str = ""
    local_alias: str | None = None
    remote_alias: str | None = None
    session_welcome: str | None = None


class WormholeDmResetRequest(BaseModel):
    peer_id: str | None = None


class WormholeDmBootstrapEncryptRequest(BaseModel):
    peer_id: str = ""
    lookup_token: str = ""
    plaintext: str


class WormholeDmBootstrapDecryptRequest(BaseModel):
    sender_id: str = ""
    ciphertext: str


class WormholeDmInviteImportRequest(BaseModel):
    invite: dict[str, Any]
    alias: str = ""


class WormholeRootWitnessImportRequest(BaseModel):
    material: dict[str, Any]


class WormholeRootWitnessImportPathRequest(BaseModel):
    path: str = ""


class WormholeRootTransparencyLedgerPublishRequest(BaseModel):
    path: str = ""
    max_records: int = 64


class WormholeDmSenderTokenRequest(BaseModel):
    recipient_id: str
    delivery_class: str
    recipient_token: str = ""
    count: int = 1


class WormholeOpenSealRequest(BaseModel):
    sender_seal: str
    candidate_dh_pub: str = ""
    recipient_id: str
    expected_msg_id: str


class WormholeBuildSealRequest(BaseModel):
    recipient_id: str
    recipient_dh_pub: str = ""
    msg_id: str
    timestamp: int


class WormholeDeadDropTokenRequest(BaseModel):
    peer_id: str
    peer_dh_pub: str = ""
    peer_ref: str = ""


class WormholePairwiseAliasRequest(BaseModel):
    peer_id: str
    peer_dh_pub: str = ""


class WormholePairwiseAliasRotateRequest(BaseModel):
    peer_id: str
    peer_dh_pub: str = ""
    grace_ms: int = PAIRWISE_ALIAS_GRACE_DEFAULT_MS
    reason: str = AliasRotationReason.MANUAL.value


class WormholeDeadDropContactsRequest(BaseModel):
    contacts: list[dict[str, Any]]
    limit: int = 24


class WormholeSasRequest(BaseModel):
    peer_id: str
    peer_dh_pub: str = ""
    words: int = 8
    peer_ref: str = ""


class WormholeSasConfirmRequest(BaseModel):
    peer_id: str
    sas_phrase: str = ""
    peer_ref: str = ""
    words: int = 8


class WormholeGateRequest(BaseModel):
    gate_id: str
    rotate: bool = False


class WormholeGatePersonaCreateRequest(BaseModel):
    gate_id: str
    label: str = ""


class WormholeGatePersonaActivateRequest(BaseModel):
    gate_id: str
    persona_id: str


class WormholeGateKeyGrantRequest(BaseModel):
    gate_id: str
    recipient_node_id: str
    recipient_dh_pub: str
    recipient_scope: str = "member"


class WormholeGateComposeRequest(BaseModel):
    gate_id: str
    plaintext: str
    reply_to: str = ""
    compat_plaintext: bool = False


class WormholeGateEncryptedSignRequest(BaseModel):
    gate_id: str
    epoch: int = 0
    ciphertext: str
    nonce: str
    format: str = "mls1"
    reply_to: str = ""
    compat_reply_to: bool = False
    recovery_plaintext: str = ""
    envelope_hash: str = ""
    transport_lock: str = "private_strong"


class WormholeGateEncryptedPostRequest(BaseModel):
    gate_id: str
    sender_id: str
    public_key: str
    public_key_algo: str
    signature: str
    sequence: int = 0
    protocol_version: str = ""
    epoch: int = 0
    ciphertext: str
    nonce: str
    sender_ref: str
    format: str = "mls1"
    gate_envelope: str = ""
    envelope_hash: str = ""
    transport_lock: str = "private_strong"
    reply_to: str = ""
    compat_reply_to: bool = False


class WormholeGateDecryptRequest(BaseModel):
    gate_id: str
    epoch: int = 0
    ciphertext: str
    nonce: str = ""
    sender_ref: str = ""
    format: str = "mls1"
    gate_envelope: str = ""
    envelope_hash: str = ""
    recovery_envelope: bool = False
    compat_decrypt: bool = False
    event_id: str = ""


class WormholeGateDecryptBatchRequest(BaseModel):
    messages: list[WormholeGateDecryptRequest]


class WormholeGateRotateRequest(BaseModel):
    gate_id: str
    reason: str = "manual_rotate"

def _default_dm_local_alias(peer_id: str = "") -> str:
    """Generate a per-peer pseudonymous alias for DM conversations."""
    import hashlib
    import hmac as _hmac

    identity = get_dm_identity()
    node_id = str(identity.get("node_id", "") or "").strip()
    if not node_id:
        return "dm-local"
    if not peer_id:
        return node_id[:12]
    derived = _hmac.new(
        node_id.encode("utf-8"),
        peer_id.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()[:12]
    return f"dm-{derived}"


def _preferred_remote_dm_alias(peer_id: str) -> str:
    candidate = str(peer_id or "").strip()
    if not candidate:
        return ""
    try:
        from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts

        contact = dict(list_wormhole_dm_contacts().get(candidate) or {})
        shared_alias = str(contact.get("sharedAlias", "") or "").strip()
        if shared_alias:
            return shared_alias
    except Exception:
        pass
    return candidate


def _resolve_dm_aliases(
    *,
    peer_id: str,
    local_alias: str | None,
    remote_alias: str | None,
) -> tuple[str, str]:
    resolved_local = str(local_alias or "").strip() or _default_dm_local_alias(peer_id=peer_id)
    resolved_remote = str(remote_alias or "").strip() or _preferred_remote_dm_alias(peer_id)
    return resolved_local, resolved_remote


def _get_contact_trust_level(peer_id: str) -> str:
    """Look up the current backend-authoritative trust_level for a peer."""
    try:
        from services.mesh.mesh_wormhole_contacts import get_contact_trust_level

        return get_contact_trust_level(str(peer_id or "").strip())
    except Exception:
        return "unpinned"


def _compose_bundle_matches_invite_pin(peer_id: str, bundle: dict[str, Any]) -> bool:
    """True when an invite-pinned contact already matches the supplied bundle."""
    try:
        from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts
        from services.mesh.mesh_wormhole_prekey import trust_fingerprint_for_bundle_record

        contact = dict(list_wormhole_dm_contacts().get(str(peer_id or "").strip()) or {})
        if str(contact.get("trust_level", "") or "") != "invite_pinned":
            return False
        pinned = str(
            contact.get("remotePrekeyFingerprint", "")
            or contact.get("invitePinnedTrustFingerprint", "")
            or ""
        ).strip().lower()
        if not pinned:
            return False
        bundle_record = dict(bundle or {})
        bundle_payload = dict(bundle_record.get("bundle") or bundle_record)
        candidate = str(bundle_record.get("trust_fingerprint", "") or "").strip().lower()
        if not candidate:
            candidate = str(
                trust_fingerprint_for_bundle_record(
                    {
                        "agent_id": str(peer_id or "").strip(),
                        "bundle": bundle_payload,
                        "public_key": str(bundle_record.get("public_key", "") or ""),
                        "public_key_algo": str(bundle_record.get("public_key_algo", "") or "Ed25519"),
                        "protocol_version": str(bundle_record.get("protocol_version", "") or ""),
                    }
                )
                or ""
            ).strip().lower()
        return bool(candidate and pinned == candidate)
    except Exception:
        return False


def compose_wormhole_dm(
    *,
    peer_id: str,
    peer_dh_pub: str,
    plaintext: str,
    local_alias: str | None = None,
    remote_alias: str | None = None,
    remote_prekey_bundle: dict[str, Any] | None = None,
) -> dict[str, Any]:
    prepared_alias = maybe_prepare_pairwise_dm_alias_rotation(
        peer_id=str(peer_id or "").strip(),
        peer_dh_pub=str(peer_dh_pub or "").strip(),
    )
    resolved_local, resolved_remote = _resolve_dm_aliases(
        peer_id=peer_id,
        local_alias=local_alias,
        remote_alias=remote_alias,
    )
    alias_wrapped = prepare_outbound_alias_binding_payload(
        peer_id=str(peer_id or "").strip(),
        plaintext=str(plaintext or ""),
    )
    outgoing_plaintext = str(alias_wrapped.get("plaintext", plaintext) or plaintext)
    commit_updates = dict(alias_wrapped.get("commit_updates") or {})
    _compose_trust_level = _get_contact_trust_level(peer_id)

    has_session = has_mls_dm_session(resolved_local, resolved_remote)
    if not has_session.get("ok"):
        return has_session
    if has_session.get("exists"):
        encrypted = encrypt_mls_dm(resolved_local, resolved_remote, outgoing_plaintext)
        if encrypted.get("ok"):
            if commit_updates:
                register_outbound_alias_rotation_commit(
                    peer_id=str(peer_id or "").strip(),
                    payload_format="mls1",
                    ciphertext=str(encrypted.get("ciphertext", "") or ""),
                    updates=commit_updates,
                )
            return {
                "ok": True,
                "peer_id": str(peer_id or "").strip(),
                "local_alias": resolved_local,
                "remote_alias": resolved_remote,
                "ciphertext": str(encrypted.get("ciphertext", "") or ""),
                "nonce": str(encrypted.get("nonce", "") or ""),
                "format": "mls1",
                "session_welcome": "",
                "trust_level": _compose_trust_level,
                "alias_update_embedded": bool(alias_wrapped.get("alias_update_embedded")),
                "alias_update_reason": str(alias_wrapped.get("alias_update_reason", "") or ""),
                "alias_update_seq": int(alias_wrapped.get("alias_update_seq", 0) or 0),
                "alias_prepare_rotated": bool(prepared_alias.get("rotated", False)),
            }
        if str(encrypted.get("detail", "") or "") != "session_expired":
            return encrypted

    bundle = dict(remote_prekey_bundle or {})
    if not bundle and str(peer_id or "").strip():
        fetched_bundle = fetch_dm_prekey_bundle(str(peer_id or "").strip())
        if fetched_bundle.get("ok"):
            bundle = fetched_bundle
    if bundle and str(peer_id or "").strip():
        try:
            if _compose_bundle_matches_invite_pin(str(peer_id or "").strip(), bundle):
                _compose_trust_level = "invite_pinned"
            else:
                trust_state = observe_remote_prekey_bundle(str(peer_id or "").strip(), bundle)
                _compose_trust_level = str(trust_state.get("trust_level", "") or "")
            from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement

            verified_first_contact = verified_first_contact_requirement(
                str(peer_id or "").strip(),
                trust_level=_compose_trust_level,
            )
            if not verified_first_contact.get("ok"):
                return {
                    "ok": False,
                    "peer_id": str(peer_id or "").strip(),
                    "detail": str(verified_first_contact.get("detail", "") or "verified first contact required"),
                    "trust_changed": _compose_trust_level in ("mismatch", "continuity_broken"),
                    "trust_level": str(
                        verified_first_contact.get("trust_level", "") or _compose_trust_level or "unpinned"
                    ),
                }
        except Exception as exc:
            logger.warning("remote prekey trust pin unavailable: %s", type(exc).__name__)
    try:
        from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement

        verified_first_contact = verified_first_contact_requirement(
            str(peer_id or "").strip(),
            trust_level=_compose_trust_level,
        )
        if not verified_first_contact.get("ok"):
            return {
                "ok": False,
                "peer_id": str(peer_id or "").strip(),
                "detail": str(verified_first_contact.get("detail", "") or "verified first contact required"),
                "trust_changed": _compose_trust_level in ("mismatch", "continuity_broken"),
                "trust_level": str(verified_first_contact.get("trust_level", "") or _compose_trust_level or "unpinned"),
            }
    except Exception:
        pass
    if str(bundle.get("mls_key_package", "") or "").strip():
        initiated = initiate_mls_dm_session(
            resolved_local,
            resolved_remote,
            bundle,
            str(
                peer_dh_pub
                or bundle.get("welcome_dh_pub")
                or bundle.get("identity_dh_pub_key")
                or ""
            ).strip(),
        )
        if not initiated.get("ok"):
            return initiated
        encrypted = encrypt_mls_dm(resolved_local, resolved_remote, outgoing_plaintext)
        if not encrypted.get("ok"):
            return encrypted
        if commit_updates:
            register_outbound_alias_rotation_commit(
                peer_id=str(peer_id or "").strip(),
                payload_format="mls1",
                ciphertext=str(encrypted.get("ciphertext", "") or ""),
                updates=commit_updates,
            )
        return {
            "ok": True,
            "peer_id": str(peer_id or "").strip(),
            "local_alias": resolved_local,
            "remote_alias": resolved_remote,
            "ciphertext": str(encrypted.get("ciphertext", "") or ""),
            "nonce": str(encrypted.get("nonce", "") or ""),
            "format": "mls1",
            "session_welcome": str(initiated.get("welcome", "") or ""),
            "trust_level": _compose_trust_level,
            "alias_update_embedded": bool(alias_wrapped.get("alias_update_embedded")),
            "alias_update_reason": str(alias_wrapped.get("alias_update_reason", "") or ""),
            "alias_update_seq": int(alias_wrapped.get("alias_update_seq", 0) or 0),
            "alias_prepare_rotated": bool(prepared_alias.get("rotated", False)),
        }

    from services.wormhole_supervisor import get_transport_tier

    current_tier = get_transport_tier()
    if str(current_tier or "").startswith("private_"):
        return {
            "ok": False,
            "detail": "MLS session required in private transport mode - legacy DM fallback blocked",
        }
    contact: dict[str, Any] = {}
    resolved_peer_dh_pub = str(peer_dh_pub or "").strip()
    if not resolved_peer_dh_pub and str(peer_id or "").strip():
        try:
            from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts

            contact = dict(list_wormhole_dm_contacts().get(str(peer_id or "").strip()) or {})
            resolved_peer_dh_pub = str(
                contact.get("dhPubKey") or contact.get("invitePinnedDhPubKey") or ""
            ).strip()
        except Exception:
            contact = {}
            resolved_peer_dh_pub = ""
    elif str(peer_id or "").strip():
        try:
            from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts

            contact = dict(list_wormhole_dm_contacts().get(str(peer_id or "").strip()) or {})
        except Exception:
            contact = {}
    if str(contact.get("invitePinnedPrekeyLookupHandle", "") or "").strip():
        return {
            "ok": False,
            "peer_id": str(peer_id or "").strip(),
            "detail": "invite-scoped bootstrap required; legacy DM fallback disabled",
            "trust_level": _compose_trust_level,
        }
    if not _legacy_dm1_allowed():
        return {
            "ok": False,
            "peer_id": str(peer_id or "").strip(),
            "detail": "legacy dm1 fallback disabled; MLS bootstrap required",
            "trust_level": _compose_trust_level,
        }
    if not resolved_peer_dh_pub:
        return {"ok": False, "detail": "peer_dh_pub required for legacy DM fallback"}

    logger.warning("legacy dm compose path used")
    legacy = encrypt_wormhole_dm(
        peer_id=str(peer_id or ""),
        peer_dh_pub=resolved_peer_dh_pub,
        plaintext=outgoing_plaintext,
    )
    if not legacy.get("ok"):
        return legacy
    if commit_updates:
        register_outbound_alias_rotation_commit(
            peer_id=str(peer_id or "").strip(),
            payload_format="dm1",
            ciphertext=str(legacy.get("result", "") or ""),
            updates=commit_updates,
        )
    return {
        "ok": True,
        "peer_id": str(peer_id or "").strip(),
        "local_alias": resolved_local,
        "remote_alias": resolved_remote,
        "ciphertext": str(legacy.get("result", "") or ""),
        "nonce": "",
        "format": "dm1",
        "session_welcome": "",
        "trust_level": _compose_trust_level,
        "alias_update_embedded": bool(alias_wrapped.get("alias_update_embedded")),
        "alias_update_reason": str(alias_wrapped.get("alias_update_reason", "") or ""),
        "alias_update_seq": int(alias_wrapped.get("alias_update_seq", 0) or 0),
        "alias_prepare_rotated": bool(prepared_alias.get("rotated", False)),
    }


def decrypt_wormhole_dm_envelope(
    *,
    peer_id: str,
    ciphertext: str,
    payload_format: str = "dm1",
    nonce: str = "",
    local_alias: str | None = None,
    remote_alias: str | None = None,
    session_welcome: str | None = None,
) -> dict[str, Any]:
    resolved_local, resolved_remote = _resolve_dm_aliases(
        peer_id=peer_id,
        local_alias=local_alias,
        remote_alias=remote_alias,
    )
    normalized_format = str(payload_format or "dm1").strip().lower() or "dm1"
    if normalized_format != "mls1" and is_dm_locked_to_mls(resolved_local, resolved_remote):
        return {
            "ok": False,
            "detail": "DM session is locked to MLS format",
            "required_format": "mls1",
            "current_format": normalized_format,
        }
    if normalized_format == "mls1":
        has_session = has_mls_dm_session(resolved_local, resolved_remote)
        if not has_session.get("ok"):
            return has_session
        if not has_session.get("exists"):
            ensured = ensure_mls_dm_session(
                resolved_local,
                resolved_remote,
                str(session_welcome or ""),
                identity_alias=resolved_local,
            )
            if not ensured.get("ok"):
                return ensured
        decrypted = decrypt_mls_dm(
            resolved_local,
            resolved_remote,
            str(ciphertext or ""),
            str(nonce or ""),
        )
        if not decrypted.get("ok"):
            return decrypted
        plain_text, alias_update = _unwrap_pairwise_alias_payload(str(decrypted.get("plaintext", "") or ""))
        alias_applied = False
        if alias_update:
            alias_result = apply_inbound_alias_binding_frame(
                peer_id=str(peer_id or "").strip(),
                alias_update=alias_update,
            )
            alias_applied = bool(alias_result.get("ok"))
        mark_contact_alias_reply_observed(str(peer_id or "").strip())
        response = {
            "ok": True,
            "peer_id": str(peer_id or "").strip(),
            "local_alias": resolved_local,
            "remote_alias": resolved_remote,
            "plaintext": plain_text,
            "format": "mls1",
        }
        if alias_update:
            response["alias_update_applied"] = alias_applied
        return response

    from services.wormhole_supervisor import get_transport_tier

    current_tier = get_transport_tier()
    if str(current_tier or "").startswith("private_"):
        return {
            "ok": False,
            "detail": "MLS format required in private transport mode ├óΓé¼ΓÇ¥ legacy DM decrypt blocked",
        }
    if not _legacy_dm1_allowed():
        return {
            "ok": False,
            "detail": "legacy dm1 decrypt disabled; migrate peer to MLS",
        }
    logger.warning("legacy dm decrypt path used")
    legacy = decrypt_wormhole_dm(peer_id=str(peer_id or ""), ciphertext=str(ciphertext or ""))
    if not legacy.get("ok"):
        return legacy
    plain_text, alias_update = _unwrap_pairwise_alias_payload(str(legacy.get("result", "") or ""))
    alias_applied = False
    if alias_update:
        alias_result = apply_inbound_alias_binding_frame(
            peer_id=str(peer_id or "").strip(),
            alias_update=alias_update,
        )
        alias_applied = bool(alias_result.get("ok"))
    mark_contact_alias_reply_observed(str(peer_id or "").strip())
    response = {
        "ok": True,
        "peer_id": str(peer_id or "").strip(),
        "local_alias": resolved_local,
        "remote_alias": resolved_remote,
        "plaintext": plain_text,
        "format": "dm1",
    }
    if alias_update:
        response["alias_update_applied"] = alias_applied
    return response


@app.get("/api/settings/privacy-profile")
@limiter.limit("30/minute")
async def api_get_privacy_profile(request: Request):
    data = await asyncio.to_thread(read_wormhole_settings)
    return _redact_privacy_profile_settings(
        data,
        authenticated=_scoped_view_authenticated(request, "wormhole"),
    )


@app.get("/api/settings/wormhole-status")
@limiter.limit("30/minute")
async def api_get_wormhole_status(request: Request):
    state = await asyncio.to_thread(get_wormhole_state)
    transport_tier = _current_private_lane_tier(state)
    if (
        transport_tier == "public_degraded"
        and bool(state.get("arti_ready"))
        and _is_debug_test_request(request)
    ):
        transport_tier = "private_strong"
    authenticated = _scoped_view_authenticated(request, "wormhole")
    full_state = {
        **state,
        "transport_tier": transport_tier,
    }
    _resume_private_delivery_background_work(
        current_tier=transport_tier,
        reason="startup_resume",
    )
    full_state["private_lane_readiness"] = private_transport_manager.observe_state(
        current_tier=transport_tier,
    )
    full_state["local_custody"] = local_custody_status_snapshot()
    lookup_handle_rotation = {
        **lookup_handle_rotation_status_snapshot(),
        "last_refresh_ok": False,
    }
    private_delivery_exposure = metadata_exposure_for_request(
        request,
        authenticated=authenticated,
    )
    if authenticated:
        contact_preference_refresh = await asyncio.to_thread(
            _upgrade_invite_scoped_contact_preferences_background
        )
        rotation_refresh = await asyncio.to_thread(
            _refresh_lookup_handle_rotation_background,
            reason="status_surface",
        )
        lookup_handle_rotation = {
            **lookup_handle_rotation_status_snapshot(),
            "last_refresh_ok": bool(rotation_refresh.get("ok", False)),
        }
        privacy_core = _privacy_core_status()
        diagnostic_package = _diagnostic_review_package_snapshot(
            current_tier=transport_tier,
            local_custody=full_state.get("local_custody"),
            privacy_core=privacy_core,
            contact_preference_refresh=contact_preference_refresh,
            lookup_handle_rotation=lookup_handle_rotation,
        )
        full_state["privacy_core"] = privacy_core
        full_state["strong_claims"] = diagnostic_package.get("strong_claims")
        full_state["release_gate"] = diagnostic_package.get("release_gate")
        full_state["privacy_status"] = diagnostic_package.get("privacy_status")
        if private_delivery_exposure == "diagnostic":
            full_state["privacy_claims"] = diagnostic_package.get("claim_surface", {}).get("privacy_claims")
            full_state["rollout_readiness"] = diagnostic_package.get("rollout_readiness")
            full_state["rollout_controls"] = diagnostic_package.get("rollout_controls")
            full_state["rollout_health"] = diagnostic_package.get("rollout_health")
            full_state["claim_surface_sources"] = diagnostic_package.get("claim_surface_sources")
            full_state["review_export"] = diagnostic_package.get("review_export")
            full_state["final_review_bundle"] = diagnostic_package.get("final_review_bundle")
            full_state["staged_rollout_telemetry"] = diagnostic_package.get("staged_rollout_telemetry")
            full_state["release_claims_matrix"] = diagnostic_package.get("release_claims_matrix")
            full_state["release_checklist"] = diagnostic_package.get("release_checklist")
    return _redact_wormhole_status(
        full_state,
        authenticated=authenticated,
    )


@app.post("/api/wormhole/join")
@limiter.limit("10/minute")
async def api_wormhole_join(request: Request):
    existing = read_wormhole_settings()
    updated = write_wormhole_settings(
        enabled=True,
        transport="tor_arti",
        socks_proxy=f"socks5h://127.0.0.1:{int(get_settings().MESH_ARTI_SOCKS_PORT or 9050)}",
        socks_dns=True,
        anonymous_mode=True,
    )
    transport_changed = (
        str(existing.get("transport", "direct")) != "tor_arti"
        or str(existing.get("socks_proxy", "")) != str(updated.get("socks_proxy", ""))
        or bool(existing.get("socks_dns", True)) is not True
        or bool(existing.get("anonymous_mode", False)) is not True
        or bool(existing.get("enabled", False)) is not True
    )
    tor_result: dict[str, Any] = {"ok": False, "detail": "not started"}
    try:
        from services.tor_hidden_service import tor_service
        from routers.ai_intel import _write_env_value

        tor_result = await asyncio.to_thread(tor_service.start)
        if tor_result.get("ok"):
            _write_env_value("MESH_ARTI_ENABLED", "true")
            get_settings.cache_clear()
    except Exception as exc:
        tor_result = {"ok": False, "detail": str(exc or type(exc).__name__)}
    bootstrap_wormhole_identity()
    bootstrap_wormhole_persona_state()
    state = (
        restart_wormhole(reason="join_wormhole")
        if transport_changed
        else connect_wormhole(reason="join_wormhole")
    )

    # Enable node participation so the sync/push workers connect to peers.
    # This is the voluntary opt-in ├óΓé¼ΓÇ¥ the node only joins the network when
    # the user explicitly opens the Wormhole.
    from services.node_settings import write_node_settings

    write_node_settings(enabled=True)
    _refresh_node_peer_store()

    return {
        "ok": True,
        "identity": get_transport_identity(),
        "runtime": state,
        "settings": updated,
        "tor": tor_result,
    }


@app.post("/api/wormhole/leave")
@limiter.limit("10/minute")
async def api_wormhole_leave(request: Request):
    updated = write_wormhole_settings(enabled=False)
    state = disconnect_wormhole(reason="leave_wormhole")

    # Leaving private DM mode must not disable Infonet participation. Infonet
    # sync has its own private transport warmup and can remain connected to
    # seed/peer nodes while MeshChat stays separately opt-in.

    return {
        "ok": True,
        "runtime": state,
        "settings": updated,
    }


@app.get("/api/wormhole/identity", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_identity(request: Request):
    try:
        bootstrap_wormhole_persona_state()
        await asyncio.to_thread(_upgrade_invite_scoped_contact_preferences_background)
        await asyncio.to_thread(_refresh_lookup_handle_rotation_background, reason="transport_identity_surface")
        return get_transport_identity()
    except Exception as exc:
        logger.exception("wormhole transport identity fetch failed")
        raise HTTPException(status_code=500, detail="wormhole_identity_failed") from exc


@app.post("/api/wormhole/identity/bootstrap", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
async def api_wormhole_identity_bootstrap(request: Request):
    bootstrap_wormhole_identity()
    bootstrap_wormhole_persona_state()
    identity = get_transport_identity()
    dm_key = register_wormhole_dm_key()
    prekeys = register_wormhole_prekey_bundle()
    return {
        **identity,
        "dm_key_ok": bool(dm_key.get("ok")),
        "dm_key_detail": dm_key,
        "prekeys_ok": bool(prekeys.get("ok")),
        "prekey_detail": prekeys,
        "dm_ready": bool(dm_key.get("ok")) and bool(prekeys.get("ok")),
    }


@app.get("/api/wormhole/dm/identity", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_identity(request: Request):
    try:
        bootstrap_wormhole_persona_state()
        await asyncio.to_thread(_upgrade_invite_scoped_contact_preferences_background)
        await asyncio.to_thread(_refresh_lookup_handle_rotation_background, reason="dm_identity_surface")
        return get_dm_identity()
    except Exception as exc:
        logger.exception("wormhole dm identity fetch failed")
        raise HTTPException(status_code=500, detail="wormhole_dm_identity_failed") from exc


@app.get("/api/wormhole/dm/invite", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_invite(
    request: Request,
    label: str = Query("", max_length=96),
    expires_in_s: int = Query(0, ge=0, le=2_592_000),
):
    return export_wormhole_dm_invite(label=label, expires_in_s=expires_in_s)


@app.get("/api/wormhole/dm/invite/handles", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_invite_handles(request: Request):
    return list_prekey_lookup_handle_records_for_ui()


@app.delete("/api/wormhole/dm/invite/handles/{handle}", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_invite_handle_revoke(request: Request, handle: str):
    return revoke_prekey_lookup_handle(handle)


@app.post("/api/wormhole/dm/invite/import", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_invite_import(request: Request, body: WormholeDmInviteImportRequest):
    return import_wormhole_dm_invite(
        dict(body.invite or {}),
        alias=str(body.alias or "").strip(),
    )


def _dm_root_operator_summary(distribution: dict[str, Any], transparency: dict[str, Any]) -> dict[str, Any]:
    def _warning_window_s(configured: int, freshness_window_s: int) -> int:
        window = max(0, _safe_int(freshness_window_s or 0, 0))
        explicit = max(0, _safe_int(configured or 0, 0))
        if window <= 0:
            return explicit
        if explicit <= 0 or explicit >= window:
            if window <= 1:
                return window
            return max(1, min(window - 1, int(window * 0.75)))
        return explicit

    def _append_alert(
        alerts: list[dict[str, Any]],
        *,
        code: str,
        severity: str,
        detail: str,
        action: str,
        target: str,
        blocking: bool,
        age_s: int = 0,
        warning_window_s: int = 0,
        freshness_window_s: int = 0,
    ) -> None:
        alert: dict[str, Any] = {
            "code": str(code or "").strip(),
            "severity": str(severity or "warning").strip(),
            "detail": str(detail or "").strip(),
            "action": str(action or "").strip(),
            "target": str(target or "").strip(),
            "blocking": bool(blocking),
        }
        if age_s > 0:
            alert["age_s"] = _safe_int(age_s, 0)
        if warning_window_s > 0:
            alert["warning_window_s"] = _safe_int(warning_window_s, 0)
        if freshness_window_s > 0:
            alert["freshness_window_s"] = _safe_int(freshness_window_s, 0)
        alerts.append(alert)

    witness_state = str(distribution.get("external_witness_operator_state", "not_configured") or "not_configured")
    transparency_state = str(transparency.get("ledger_operator_state", "not_configured") or "not_configured")
    witness_configured = bool(distribution.get("external_witness_source_configured", False))
    transparency_configured = bool(transparency.get("ledger_readback_configured", False))
    witness_detail = str(distribution.get("external_witness_refresh_detail", "") or "").strip()
    transparency_detail = str(transparency.get("ledger_readback_detail", "") or "").strip()
    witness_age_s = _safe_int(distribution.get("external_witness_source_age_s", 0) or 0, 0)
    transparency_age_s = _safe_int(transparency.get("ledger_readback_export_age_s", 0) or 0, 0)
    witness_freshness_window_s = _safe_int(distribution.get("external_witness_freshness_window_s", 0) or 0, 0)
    transparency_freshness_window_s = _safe_int(transparency.get("ledger_freshness_window_s", 0) or 0, 0)
    witness_warning_window_s = _warning_window_s(
        getattr(get_settings(), "MESH_DM_ROOT_EXTERNAL_WITNESS_WARN_AGE_S", 0),
        witness_freshness_window_s,
    )
    transparency_warning_window_s = _warning_window_s(
        getattr(get_settings(), "MESH_DM_ROOT_TRANSPARENCY_LEDGER_WARN_AGE_S", 0),
        transparency_freshness_window_s,
    )
    witness_warning_due = bool(
        witness_state == "current"
        and witness_warning_window_s > 0
        and witness_age_s >= witness_warning_window_s
    )
    transparency_warning_due = bool(
        transparency_state == "current"
        and transparency_warning_window_s > 0
        and transparency_age_s >= transparency_warning_window_s
    )
    witness_attention = bool(distribution.get("external_witness_reacquire_required", False)) or witness_state in {
        "stale",
        "error",
        "descriptors_only",
    } or witness_warning_due
    transparency_attention = bool(transparency.get("ledger_external_verification_required", False)) or transparency_state in {
        "stale",
        "error",
    } or transparency_warning_due
    any_external_configured = bool(witness_configured or transparency_configured)
    external_assurance_current = witness_state == "current" and transparency_state == "current"
    requires_attention = bool(witness_attention or transparency_attention)
    if external_assurance_current:
        state = "current_external"
        detail = "configured external witness and transparency assurances are current"
        if witness_warning_due or transparency_warning_due:
            detail = "configured external assurance is current but approaching freshness limit"
    elif any_external_configured and requires_attention:
        state = "stale_external"
        detail = "configured external witness or transparency assurance requires refresh"
    else:
        state = "local_cached_only"
        detail = "external witness and transparency assurance are not fully configured"
    if witness_state == "error" or transparency_state == "error":
        health_state = "error"
    elif state == "stale_external":
        health_state = "stale"
    elif witness_warning_due or transparency_warning_due:
        health_state = "warning"
    elif state == "current_external":
        health_state = "ok"
    else:
        health_state = "warning"
    witness_health_state = (
        "warning"
        if witness_state == "current" and witness_warning_due
        else
        "ok"
        if witness_state == "current"
        else "error"
        if witness_state == "error"
        else "stale"
        if witness_state in {"stale", "descriptors_only"}
        else "warning"
    )
    transparency_health_state = (
        "warning"
        if transparency_state == "current" and transparency_warning_due
        else
        "ok"
        if transparency_state == "current"
        else "error"
        if transparency_state == "error"
        else "stale"
        if transparency_state == "stale"
        else "warning"
    )
    strong_trust_blocked = bool(
        (witness_configured and witness_state != "current")
        or (transparency_configured and transparency_state != "current")
    )
    alerts: list[dict[str, Any]] = []
    witness_detail_lower = witness_detail.lower()
    transparency_detail_lower = transparency_detail.lower()
    if not witness_configured:
        _append_alert(
            alerts,
            code="external_witness_not_configured",
            severity="warning",
            detail="external witness source is not configured",
            action="configure_external_witness_source",
            target="external_witness",
            blocking=False,
        )
    elif witness_state == "descriptors_only":
        _append_alert(
            alerts,
            code="external_witness_receipts_missing",
            severity="stale",
            detail=witness_detail or "external witness descriptors are present but current-manifest receipts are missing",
            action="reacquire_external_witness_receipts",
            target="external_witness",
            blocking=True,
        )
    elif witness_state == "stale":
        if any(
            marker in witness_detail_lower
            for marker in (
                "manifest_fingerprint mismatch",
                "waiting for current-manifest receipts",
            )
        ):
            _append_alert(
                alerts,
                code="external_witness_receipts_stale",
                severity="stale",
                detail=witness_detail or "external witness receipts do not match the current manifest",
                action="reacquire_external_witness_receipts",
                target="external_witness",
                blocking=True,
            )
        else:
            _append_alert(
                alerts,
                code="external_witness_source_stale",
                severity="stale",
                detail=witness_detail or "external witness source is stale",
                action="refresh_external_witness_source",
                target="external_witness",
                blocking=True,
                age_s=witness_age_s,
                warning_window_s=witness_warning_window_s,
                freshness_window_s=witness_freshness_window_s,
            )
    elif witness_state == "error":
        _append_alert(
            alerts,
            code="external_witness_source_error",
            severity="error",
            detail=witness_detail or "external witness source refresh failed",
            action="check_external_witness_source",
            target="external_witness",
            blocking=True,
            age_s=witness_age_s,
            warning_window_s=witness_warning_window_s,
            freshness_window_s=witness_freshness_window_s,
        )
    elif witness_warning_due:
        _append_alert(
            alerts,
            code="external_witness_age_warning",
            severity="warning",
            detail="external witness source is current but approaching the freshness limit",
            action="refresh_external_witness_source",
            target="external_witness",
            blocking=False,
            age_s=witness_age_s,
            warning_window_s=witness_warning_window_s,
            freshness_window_s=witness_freshness_window_s,
        )
    if not transparency_configured:
        _append_alert(
            alerts,
            code="external_transparency_not_configured",
            severity="warning",
            detail="external transparency readback is not configured",
            action="configure_external_transparency_readback",
            target="external_transparency",
            blocking=False,
        )
    elif transparency_state == "stale":
        if any(
            marker in transparency_detail_lower
            for marker in (
                "head mismatch",
                "binding mismatch",
                "external ledger stale",
                "exported_at required",
            )
        ):
            _append_alert(
                alerts,
                code="external_transparency_stale",
                severity="stale",
                detail=transparency_detail or "external transparency ledger is stale or mismatched",
                action="republish_transparency_ledger",
                target="external_transparency",
                blocking=True,
                age_s=transparency_age_s,
                warning_window_s=transparency_warning_window_s,
                freshness_window_s=transparency_freshness_window_s,
            )
        else:
            _append_alert(
                alerts,
                code="external_transparency_readback_stale",
                severity="stale",
                detail=transparency_detail or "external transparency readback requires verification",
                action="verify_external_readback",
                target="external_transparency",
                blocking=True,
                age_s=transparency_age_s,
                warning_window_s=transparency_warning_window_s,
                freshness_window_s=transparency_freshness_window_s,
            )
    elif transparency_state == "error":
        _append_alert(
            alerts,
            code="external_transparency_readback_error",
            severity="error",
            detail=transparency_detail or "external transparency readback failed",
            action="check_external_transparency_readback",
            target="external_transparency",
            blocking=True,
            age_s=transparency_age_s,
            warning_window_s=transparency_warning_window_s,
            freshness_window_s=transparency_freshness_window_s,
        )
    elif transparency_warning_due:
        _append_alert(
            alerts,
            code="external_transparency_age_warning",
            severity="warning",
            detail="external transparency ledger is current but approaching the freshness limit",
            action="republish_transparency_ledger",
            target="external_transparency",
            blocking=False,
            age_s=transparency_age_s,
            warning_window_s=transparency_warning_window_s,
            freshness_window_s=transparency_freshness_window_s,
        )
    seen_actions: set[str] = set()
    deduped_actions: list[str] = []
    runbook_actions: list[dict[str, Any]] = []
    for alert in alerts:
        action = str(alert.get("action", "") or "").strip()
        target = str(alert.get("target", "") or "").strip()
        key = f"{action}:{target}"
        if action and action not in deduped_actions:
            deduped_actions.append(action)
        if not action or key in seen_actions:
            continue
        seen_actions.add(key)
        runbook_actions.append(
            {
                "action": action,
                "target": target,
                "severity": str(alert.get("severity", "warning") or "warning").strip(),
                "blocking": bool(alert.get("blocking", False)),
                "reason": str(alert.get("detail", "") or "").strip(),
            }
        )
    next_action = ""
    for item in runbook_actions:
        if item.get("blocking"):
            next_action = str(item.get("action", "") or "").strip()
            break
    if not next_action and runbook_actions:
        next_action = str(runbook_actions[0].get("action", "") or "").strip()
    blocking_alert_count = sum(1 for alert in alerts if bool(alert.get("blocking", False)))
    warning_alert_count = sum(
        1 for alert in alerts if str(alert.get("severity", "") or "").strip() == "warning"
    )
    return {
        "state": state,
        "detail": detail,
        "health_state": health_state,
        "witness_health_state": witness_health_state,
        "transparency_health_state": transparency_health_state,
        "external_assurance_current": external_assurance_current,
        "external_assurance_configured": bool(witness_configured and transparency_configured),
        "requires_attention": requires_attention,
        "strong_trust_blocked": strong_trust_blocked,
        "warning_due": bool(witness_warning_due or transparency_warning_due),
        "witness_warning_due": witness_warning_due,
        "transparency_warning_due": transparency_warning_due,
        "witness_warning_window_s": witness_warning_window_s,
        "transparency_warning_window_s": transparency_warning_window_s,
        "recommended_actions": deduped_actions,
        "next_action": next_action,
        "alerts": alerts,
        "alert_count": len(alerts),
        "blocking_alert_count": blocking_alert_count,
        "warning_alert_count": warning_alert_count,
        "runbook_actions": runbook_actions,
        "witness_state": witness_state,
        "witness_detail": witness_detail,
        "transparency_state": transparency_state,
        "transparency_detail": transparency_detail,
        "independent_quorum_met": bool(distribution.get("witness_independent_quorum_met", False)),
        "witness_configured": witness_configured,
        "transparency_configured": transparency_configured,
    }


def _dm_root_monitoring_view(summary: dict[str, Any]) -> dict[str, Any]:
    alerts = [dict(item or {}) for item in list(summary.get("alerts") or []) if isinstance(item, dict)]
    runbook_actions = [dict(item or {}) for item in list(summary.get("runbook_actions") or []) if isinstance(item, dict)]
    strong_trust_blocked = bool(summary.get("strong_trust_blocked", False))
    health_state = str(summary.get("health_state", "warning") or "warning").strip().lower()
    summary_state = str(summary.get("state", "local_cached_only") or "local_cached_only").strip().lower()
    if strong_trust_blocked or health_state in {"error", "stale"}:
        monitor_state = "critical"
    elif health_state == "warning":
        monitor_state = "warning"
    else:
        monitor_state = "ok"
    page_required = bool(monitor_state == "critical")
    ticket_required = bool(monitor_state == "warning" or page_required)
    primary_alert = next((item for item in alerts if bool(item.get("blocking", False))), alerts[0] if alerts else {})
    if page_required:
        status_line = "DM root external assurance is blocking strong trust and needs operator action"
    elif ticket_required:
        status_line = "DM root external assurance needs operator attention soon"
    else:
        status_line = "DM root external assurance is healthy"
    recommended_check_interval_s = 60 if page_required else 300 if ticket_required else 900
    return {
        "state": monitor_state,
        "page_required": page_required,
        "ticket_required": ticket_required,
        "runbook_required": bool(runbook_actions),
        "strong_trust_blocked": strong_trust_blocked,
        "status_line": status_line,
        "summary_state": summary_state,
        "summary_health_state": health_state,
        "primary_alert": primary_alert,
        "active_alert_codes": [
            str(item.get("code", "") or "").strip()
            for item in alerts
            if str(item.get("code", "") or "").strip()
        ],
        "recommended_check_interval_s": recommended_check_interval_s,
    }


def _dm_root_runbook_action_detail(
    action: str,
    *,
    target: str,
    severity: str,
    blocking: bool,
    reason: str,
) -> dict[str, Any]:
    action_key = str(action or "").strip()
    target_key = str(target or "").strip()
    severity_key = str(severity or "warning").strip().lower()
    templates: dict[str, dict[str, Any]] = {
        "configure_external_witness_source": {
            "title": "Configure external witness source",
            "summary": "Point DM root witness refresh at an independently managed witness package source.",
            "steps": [
                "Choose an external witness package source URI or file path that is managed outside the local runtime.",
                "Set MESH_DM_ROOT_EXTERNAL_WITNESS_IMPORT_URI or MESH_DM_ROOT_EXTERNAL_WITNESS_IMPORT_PATH.",
                "Confirm the source publishes descriptors and current-manifest receipts for the active root manifest.",
            ],
        },
        "reacquire_external_witness_receipts": {
            "title": "Reacquire external witness receipts",
            "summary": "Refresh current-manifest witness receipts so the active root manifest satisfies the external witness policy again.",
            "steps": [
                "Request fresh external witness receipts for the current published root manifest fingerprint.",
                "Restage the refreshed receipt package through the configured external witness source.",
                "Recheck /api/wormhole/dm/root-health until witness state returns to current.",
            ],
        },
        "refresh_external_witness_source": {
            "title": "Refresh external witness source",
            "summary": "Publish a fresh external witness package before the configured freshness window expires or after it has gone stale.",
            "steps": [
                "Regenerate or republish the external witness package with a fresh exported_at timestamp.",
                "Include any required current-manifest receipts for the active root manifest.",
                "Verify the configured source is readable and root health clears the warning or stale state.",
            ],
        },
        "check_external_witness_source": {
            "title": "Check external witness source",
            "summary": "Investigate why the configured external witness source is unreadable or invalid.",
            "steps": [
                "Verify the configured witness source URI or path is reachable from the backend.",
                "Validate the package schema, exported_at, descriptors, and manifest_fingerprint fields.",
                "Restore the source and confirm strong DM trust is no longer blocked.",
            ],
        },
        "configure_external_transparency_readback": {
            "title": "Configure external transparency readback",
            "summary": "Point DM root transparency verification at an externally published transparency ledger.",
            "steps": [
                "Choose an external transparency ledger readback URI or exported ledger path.",
                "Set MESH_DM_ROOT_TRANSPARENCY_LEDGER_READBACK_URI and, if needed, MESH_DM_ROOT_TRANSPARENCY_LEDGER_EXPORT_PATH.",
                "Verify the readback source exposes the current transparency binding for the active root manifest.",
            ],
        },
        "republish_transparency_ledger": {
            "title": "Republish transparency ledger",
            "summary": "Republish the stable-root transparency ledger so external readback reflects the current manifest and witness binding.",
            "steps": [
                "Publish a fresh transparency ledger export to the configured external location.",
                "Confirm the external ledger head and binding match the current manifest and witness set.",
                "Recheck /api/wormhole/dm/root-health until transparency state returns to current.",
            ],
        },
        "verify_external_readback": {
            "title": "Verify external transparency readback",
            "summary": "Investigate why external transparency readback is stale or incomplete for the current root binding.",
            "steps": [
                "Confirm the configured readback URI is reachable and serving the latest ledger export.",
                "Validate the exported ledger chain and current head binding fingerprint.",
                "Restore readback visibility and verify the health endpoint clears the transparency alert.",
            ],
        },
        "check_external_transparency_readback": {
            "title": "Check external transparency readback",
            "summary": "Investigate why the configured external transparency readback source is unreadable or invalid.",
            "steps": [
                "Verify the configured ledger readback URI or file path is reachable from the backend.",
                "Validate the exported ledger JSON and chain integrity at the source.",
                "Restore the source and confirm transparency verification returns to current.",
            ],
        },
    }
    template = dict(templates.get(action_key) or {})
    if blocking:
        urgency = "page"
    elif severity_key == "warning":
        urgency = "watch"
    else:
        urgency = "ticket"
    return {
        "action": action_key,
        "target": target_key,
        "severity": severity_key or "warning",
        "blocking": bool(blocking),
        "urgency": urgency,
        "title": str(template.get("title", action_key.replace("_", " ").title()) or action_key).strip(),
        "summary": str(template.get("summary", reason or action_key.replace("_", " ")) or "").strip(),
        "reason": str(reason or "").strip(),
        "steps": [str(step or "").strip() for step in list(template.get("steps") or []) if str(step or "").strip()],
        "owner": "dm_root_ops",
    }


def _dm_root_runbook_view(summary: dict[str, Any], monitoring: dict[str, Any]) -> dict[str, Any]:
    raw_actions = [dict(item or {}) for item in list(summary.get("runbook_actions") or []) if isinstance(item, dict)]
    enriched_actions = [
        _dm_root_runbook_action_detail(
            str(item.get("action", "") or "").strip(),
            target=str(item.get("target", "") or "").strip(),
            severity=str(item.get("severity", "warning") or "warning").strip(),
            blocking=bool(item.get("blocking", False)),
            reason=str(item.get("reason", "") or "").strip(),
        )
        for item in raw_actions
    ]
    next_action = str(summary.get("next_action", "") or "").strip()
    next_action_detail = next(
        (dict(item) for item in enriched_actions if str(item.get("action", "") or "").strip() == next_action),
        {},
    )
    monitor_state = str(monitoring.get("state", "warning") or "warning").strip().lower()
    summary_state = str(summary.get("state", "local_cached_only") or "local_cached_only").strip().lower()
    if monitor_state == "critical":
        urgency = "page"
    elif monitor_state == "warning" and summary_state == "local_cached_only":
        urgency = "ticket"
    elif monitor_state == "warning":
        urgency = "watch"
    else:
        urgency = "none"
    return {
        "attention_required": bool(summary.get("requires_attention", False)),
        "strong_trust_blocked": bool(summary.get("strong_trust_blocked", False)),
        "urgency": urgency,
        "status_line": str(monitoring.get("status_line", "") or "").strip(),
        "next_action": next_action,
        "next_action_detail": next_action_detail,
        "actions": enriched_actions,
    }


@app.get("/api/wormhole/dm/root-distribution", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_distribution(request: Request):
    from services.mesh.mesh_wormhole_root_manifest import get_current_root_manifest
    from services.mesh.mesh_wormhole_root_transparency import get_current_root_transparency_record

    distribution = get_current_root_manifest()
    transparency = get_current_root_transparency_record(distribution=distribution)
    return {
        **distribution,
        "dm_root_operator_summary": _dm_root_operator_summary(distribution, transparency),
    }


@app.post("/api/wormhole/dm/root-witnesses/import", dependencies=[Depends(require_admin)])
@limiter.limit("20/minute")
async def api_wormhole_dm_root_witness_import(request: Request, body: WormholeRootWitnessImportRequest):
    from services.mesh.mesh_wormhole_root_manifest import import_external_root_witness_material

    return import_external_root_witness_material(dict(body.material or {}))


@app.post("/api/wormhole/dm/root-witnesses/import-config", dependencies=[Depends(require_admin)])
@limiter.limit("20/minute")
async def api_wormhole_dm_root_witness_import_config(
    request: Request, body: WormholeRootWitnessImportPathRequest
):
    from services.mesh.mesh_wormhole_root_manifest import import_external_root_witness_material_from_file

    return import_external_root_witness_material_from_file(path=str(body.path or "").strip() or None)


@app.get("/api/wormhole/dm/root-transparency", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_transparency(request: Request):
    from services.mesh.mesh_wormhole_root_manifest import get_current_root_manifest
    from services.mesh.mesh_wormhole_root_transparency import get_current_root_transparency_record

    distribution = get_current_root_manifest()
    transparency = get_current_root_transparency_record(distribution=distribution)
    return {
        **transparency,
        "dm_root_operator_summary": _dm_root_operator_summary(distribution, transparency),
    }


@app.get("/api/wormhole/dm/root-health", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_health(request: Request):
    from services.mesh.mesh_wormhole_root_manifest import get_current_root_manifest
    from services.mesh.mesh_wormhole_root_transparency import get_current_root_transparency_record

    distribution = get_current_root_manifest()
    transparency = get_current_root_transparency_record(distribution=distribution)
    summary = _dm_root_operator_summary(distribution, transparency)
    monitoring = _dm_root_monitoring_view(summary)
    runbook = _dm_root_runbook_view(summary, monitoring)
    return {
        "ok": True,
        "checked_at": int(time.time()),
        **summary,
        "monitoring": monitoring,
        "runbook": runbook,
        "witness": {
            "state": summary.get("witness_state", "not_configured"),
            "health_state": summary.get("witness_health_state", "warning"),
            "detail": summary.get("witness_detail", ""),
            "source_ref": str(distribution.get("external_witness_refresh_source_ref", "") or "").strip(),
            "source_scope": str(distribution.get("external_witness_source_scope", "") or "").strip(),
            "source_label": str(distribution.get("external_witness_source_label", "") or "").strip(),
            "age_s": _safe_int(distribution.get("external_witness_source_age_s", 0) or 0, 0),
            "warning_window_s": _safe_int(summary.get("witness_warning_window_s", 0) or 0, 0),
            "freshness_window_s": _safe_int(distribution.get("external_witness_freshness_window_s", 0) or 0, 0),
            "manifest_matches_current": bool(distribution.get("external_witness_manifest_matches_current", False)),
            "reacquire_required": bool(distribution.get("external_witness_reacquire_required", False)),
            "independent_quorum_met": bool(distribution.get("witness_independent_quorum_met", False)),
        },
        "transparency": {
            "state": summary.get("transparency_state", "not_configured"),
            "health_state": summary.get("transparency_health_state", "warning"),
            "detail": summary.get("transparency_detail", ""),
            "source_ref": str(transparency.get("ledger_readback_source_ref", "") or "").strip(),
            "export_path": str(transparency.get("ledger_export_path", "") or "").strip(),
            "age_s": _safe_int(transparency.get("ledger_readback_export_age_s", 0) or 0, 0),
            "warning_window_s": _safe_int(summary.get("transparency_warning_window_s", 0) or 0, 0),
            "freshness_window_s": _safe_int(transparency.get("ledger_freshness_window_s", 0) or 0, 0),
            "verification_required": bool(transparency.get("ledger_external_verification_required", False)),
        },
    }


@app.get("/api/wormhole/dm/root-health/runbook", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_health_runbook(request: Request):
    from services.mesh.mesh_wormhole_root_manifest import get_current_root_manifest
    from services.mesh.mesh_wormhole_root_transparency import get_current_root_transparency_record

    distribution = get_current_root_manifest()
    transparency = get_current_root_transparency_record(distribution=distribution)
    summary = _dm_root_operator_summary(distribution, transparency)
    monitoring = _dm_root_monitoring_view(summary)
    return {
        "ok": True,
        "checked_at": int(time.time()),
        **_dm_root_runbook_view(summary, monitoring),
    }


@app.get("/api/wormhole/dm/root-health/alerts", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_health_alerts(request: Request):
    from services.mesh.mesh_wormhole_root_manifest import get_current_root_manifest
    from services.mesh.mesh_wormhole_root_transparency import get_current_root_transparency_record

    distribution = get_current_root_manifest()
    transparency = get_current_root_transparency_record(distribution=distribution)
    summary = _dm_root_operator_summary(distribution, transparency)
    monitoring = _dm_root_monitoring_view(summary)
    return {
        "ok": True,
        "checked_at": int(time.time()),
        **monitoring,
        "alerts": [dict(item or {}) for item in list(summary.get("alerts") or []) if isinstance(item, dict)],
        "alert_count": _safe_int(summary.get("alert_count", 0) or 0, 0),
        "blocking_alert_count": _safe_int(summary.get("blocking_alert_count", 0) or 0, 0),
        "warning_alert_count": _safe_int(summary.get("warning_alert_count", 0) or 0, 0),
        "next_action": str(summary.get("next_action", "") or "").strip(),
        "runbook_actions": [dict(item or {}) for item in list(summary.get("runbook_actions") or []) if isinstance(item, dict)],
    }


@app.get("/api/wormhole/dm/root-transparency/ledger", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_transparency_ledger(request: Request, max_records: int = Query(64, ge=1, le=256)):
    from services.mesh.mesh_wormhole_root_transparency import export_root_transparency_ledger

    return export_root_transparency_ledger(max_records=_safe_int(max_records or 64, 64))


@app.post("/api/wormhole/dm/root-transparency/ledger/publish", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_dm_root_transparency_ledger_publish(
    request: Request, body: WormholeRootTransparencyLedgerPublishRequest
):
    from services.mesh.mesh_wormhole_root_transparency import publish_root_transparency_ledger_to_file

    return publish_root_transparency_ledger_to_file(
        path=str(body.path or "").strip() or None,
        max_records=_safe_int(body.max_records or 64, 64),
    )


@app.get("/api/wormhole/dm/root-transparency/ledger/published", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_dm_root_transparency_ledger_published(request: Request, path: str = Query("")):
    from services.mesh.mesh_wormhole_root_transparency import read_exported_root_transparency_ledger

    return read_exported_root_transparency_ledger(path=str(path or "").strip() or None)


@app.post("/api/wormhole/sign", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_sign(request: Request, body: WormholeSignRequest):
    event_type = str(body.event_type or "")
    payload = dict(body.payload or {})
    if event_type.startswith("dm_"):
        return sign_wormhole_event(
            event_type=event_type,
            payload=payload,
            sequence=body.sequence,
        )
    gate_id = str(body.gate_id or "").strip().lower()
    if gate_id:
        signed = sign_gate_wormhole_event(
            gate_id=gate_id,
            event_type=event_type,
            payload=payload,
            sequence=body.sequence,
        )
        if not signed.get("signature"):
            raise HTTPException(status_code=400, detail=str(signed.get("detail") or "wormhole_gate_sign_failed"))
        return signed
    return sign_public_wormhole_event(
        event_type=event_type,
        payload=payload,
        sequence=body.sequence,
    )


@app.post("/api/wormhole/gate/enter", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_enter(request: Request, body: WormholeGateRequest):
    gate_id = str(body.gate_id or "")
    result = enter_gate_anonymously(gate_id, rotate=bool(body.rotate))
    if result.get("ok"):
        snapshot = export_gate_state_snapshot(gate_id)
        if snapshot.get("ok"):
            result["gate_state_snapshot"] = snapshot
        else:
            result["gate_state_snapshot_error"] = str(snapshot.get("detail") or "gate_state_export_failed")
    return result


@app.post("/api/wormhole/gate/leave", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_leave(request: Request, body: WormholeGateRequest):
    return leave_gate(str(body.gate_id or ""))


@app.get("/api/wormhole/gate/{gate_id}/identity")
@limiter.limit("30/minute")
async def api_wormhole_gate_identity(request: Request, gate_id: str):
    return get_active_gate_identity(gate_id)


@app.get("/api/wormhole/gate/{gate_id}/personas")
@limiter.limit("30/minute")
async def api_wormhole_gate_personas(request: Request, gate_id: str):
    return list_gate_personas(gate_id)


@app.get("/api/wormhole/gate/{gate_id}/key")
@limiter.limit("30/minute")
async def api_wormhole_gate_key_status(request: Request, gate_id: str):
    exposure = metadata_exposure_for_request(request, authenticated=True)
    return gate_repair_status_snapshot(gate_id, exposure=exposure)


@app.post("/api/wormhole/gate/key/rotate", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
async def api_wormhole_gate_key_rotate(request: Request, body: WormholeGateRotateRequest):
    gate_id = str(body.gate_id or "")
    result = rotate_gate_epoch(
        gate_id=gate_id,
        reason=str(body.reason or "manual_rotate"),
    )
    if result.get("ok"):
        snapshot = export_gate_state_snapshot(gate_id)
        if snapshot.get("ok"):
            result["gate_state_snapshot"] = snapshot
        else:
            result["gate_state_snapshot_error"] = str(snapshot.get("detail") or "gate_state_export_failed")
    return result


@app.post("/api/wormhole/gate/persona/create", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_persona_create(
    request: Request, body: WormholeGatePersonaCreateRequest
):
    gate_id = str(body.gate_id or "")
    result = create_gate_persona(gate_id, label=str(body.label or ""))
    if result.get("ok"):
        snapshot = export_gate_state_snapshot(gate_id)
        if snapshot.get("ok"):
            result["gate_state_snapshot"] = snapshot
        else:
            result["gate_state_snapshot_error"] = str(snapshot.get("detail") or "gate_state_export_failed")
    return result


@app.post("/api/wormhole/gate/persona/activate", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_persona_activate(
    request: Request, body: WormholeGatePersonaActivateRequest
):
    gate_id = str(body.gate_id or "")
    result = activate_gate_persona(gate_id, str(body.persona_id or ""))
    if result.get("ok"):
        snapshot = export_gate_state_snapshot(gate_id)
        if snapshot.get("ok"):
            result["gate_state_snapshot"] = snapshot
        else:
            result["gate_state_snapshot_error"] = str(snapshot.get("detail") or "gate_state_export_failed")
    return result


@app.post("/api/wormhole/gate/persona/clear", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_persona_clear(request: Request, body: WormholeGateRequest):
    gate_id = str(body.gate_id or "")
    result = clear_active_gate_persona(gate_id)
    if result.get("ok"):
        snapshot = export_gate_state_snapshot(gate_id)
        if snapshot.get("ok"):
            result["gate_state_snapshot"] = snapshot
        else:
            result["gate_state_snapshot_error"] = str(snapshot.get("detail") or "gate_state_export_failed")
    return result


@app.post("/api/wormhole/gate/persona/retire", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_persona_retire(
    request: Request, body: WormholeGatePersonaActivateRequest
):
    gate_id = str(body.gate_id or "")
    result = retire_gate_persona(gate_id, str(body.persona_id or ""))
    if result.get("ok"):
        result["gate_key_status"] = mark_gate_rekey_recommended(
            gate_id,
            reason="persona_retired",
        )
        snapshot = export_gate_state_snapshot(gate_id)
        if snapshot.get("ok"):
            result["gate_state_snapshot"] = snapshot
        else:
            result["gate_state_snapshot_error"] = str(snapshot.get("detail") or "gate_state_export_failed")
    return result


@app.post("/api/wormhole/gate/key/grant", dependencies=[Depends(require_local_operator)])
@limiter.limit("20/minute")
async def api_wormhole_gate_key_grant(request: Request, body: WormholeGateKeyGrantRequest):
    return ensure_gate_member_access(
        gate_id=str(body.gate_id or ""),
        recipient_node_id=str(body.recipient_node_id or ""),
        recipient_dh_pub=str(body.recipient_dh_pub or ""),
        recipient_scope=str(body.recipient_scope or "member"),
    )


def _backend_gate_plaintext_guard(
    *,
    gate_id: str,
    compat_plaintext: bool,
) -> dict[str, Any] | None:
    # These endpoints are already guarded by require_local_operator and are
    # the atomic local-control path that encrypts/signs before append. They
    # must remain available as the durable-envelope recovery path when the
    # browser/native split cannot carry gate_envelope material.
    return None


def _backend_gate_encrypted_reply_to_guard(
    *,
    gate_id: str,
    reply_to: str,
    compat_reply_to: bool,
) -> dict[str, Any] | None:
    reply_to_val = str(reply_to or "").strip()
    if not reply_to_val or compat_reply_to:
        return None
    return {
        "ok": False,
        "detail": "gate_encrypted_reply_to_hidden_required",
        "gate_id": gate_id,
        "compat_reply_to": False,
    }


@app.post("/api/wormhole/gate/message/compose", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_gate_message_compose(request: Request, body: WormholeGateComposeRequest):
    blocked = _backend_gate_plaintext_guard(
        gate_id=str(body.gate_id or ""),
        compat_plaintext=bool(body.compat_plaintext),
    )
    if blocked is not None:
        return blocked
    composed = compose_gate_message_with_repair(
        gate_id=str(body.gate_id or ""),
        plaintext=str(body.plaintext or ""),
        reply_to=str(body.reply_to or ""),
    )
    if composed.get("ok") and _is_debug_test_request(request):
        return {**dict(composed), "epoch": composed.get("epoch", 0)}
    if composed.get("ok"):
        return _redact_composed_gate_message(composed)
    return composed


@app.post("/api/wormhole/gate/message/sign-encrypted", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_gate_message_sign_encrypted(
    request: Request,
    body: WormholeGateEncryptedSignRequest,
):
    blocked = _backend_gate_encrypted_reply_to_guard(
        gate_id=str(body.gate_id or ""),
        reply_to=str(body.reply_to or ""),
        compat_reply_to=bool(body.compat_reply_to),
    )
    if blocked is not None:
        return blocked
    signed = sign_gate_message_with_repair(
        gate_id=str(body.gate_id or ""),
        epoch=_safe_int(body.epoch or 0),
        ciphertext=str(body.ciphertext or ""),
        nonce=str(body.nonce or ""),
        payload_format=str(body.format or "mls1"),
        reply_to=str(body.reply_to or ""),
        compat_reply_to=bool(body.compat_reply_to),
        recovery_plaintext=str(getattr(body, "recovery_plaintext", "") or ""),
        envelope_hash=str(body.envelope_hash or ""),
        transport_lock=str(getattr(body, "transport_lock", "private_strong") or "private_strong"),
    )
    if signed.get("ok") and _is_debug_test_request(request):
        return signed
    if signed.get("ok"):
        return _redact_signed_gate_message(signed)
    return signed


@app.post("/api/wormhole/gate/message/post-encrypted")
@limiter.limit("30/minute")
async def api_wormhole_gate_message_post_encrypted(
    request: Request,
    body: WormholeGateEncryptedPostRequest,
):
    blocked = _backend_gate_encrypted_reply_to_guard(
        gate_id=str(body.gate_id or ""),
        reply_to=str(body.reply_to or ""),
        compat_reply_to=bool(body.compat_reply_to),
    )
    if blocked is not None:
        return blocked
    return _submit_gate_message_envelope(
        request,
        str(body.gate_id or ""),
        {
            "sender_id": str(body.sender_id or ""),
            "public_key": str(body.public_key or ""),
            "public_key_algo": str(body.public_key_algo or ""),
            "signature": str(body.signature or ""),
            "sequence": _safe_int(body.sequence or 0),
            "protocol_version": str(body.protocol_version or ""),
            "epoch": _safe_int(body.epoch or 0),
            "ciphertext": str(body.ciphertext or ""),
            "nonce": str(body.nonce or ""),
            "sender_ref": str(body.sender_ref or ""),
            "format": str(body.format or "mls1"),
            "gate_envelope": str(body.gate_envelope or ""),
            "envelope_hash": str(body.envelope_hash or ""),
            "transport_lock": str(getattr(body, "transport_lock", "private_strong") or "private_strong"),
            "reply_to": str(body.reply_to or ""),
        },
    )


@app.post("/api/wormhole/gate/message/post", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_gate_message_post(request: Request, body: WormholeGateComposeRequest):
    blocked = _backend_gate_plaintext_guard(
        gate_id=str(body.gate_id or ""),
        compat_plaintext=bool(body.compat_plaintext),
    )
    if blocked is not None:
        return blocked
    composed = compose_gate_message_with_repair(
        gate_id=str(body.gate_id or ""),
        plaintext=str(body.plaintext or ""),
        reply_to=str(body.reply_to or ""),
    )
    if not composed.get("ok"):
        return composed
    reply_to = str(body.reply_to or "").strip()
    return _submit_gate_message_envelope(
        request,
        str(body.gate_id or ""),
        {
            "sender_id": composed.get("sender_id", ""),
            "public_key": composed.get("public_key", ""),
            "public_key_algo": composed.get("public_key_algo", ""),
            "signature": composed.get("signature", ""),
            "sequence": composed.get("sequence", 0),
            "protocol_version": composed.get("protocol_version", ""),
            "epoch": composed.get("epoch", 0),
            "ciphertext": composed.get("ciphertext", ""),
            "nonce": composed.get("nonce", ""),
            "sender_ref": composed.get("sender_ref", ""),
            "format": composed.get("format", "mls1"),
            "gate_envelope": composed.get("gate_envelope", ""),
            "envelope_hash": composed.get("envelope_hash", ""),
            "transport_lock": composed.get("transport_lock", "private_strong"),
            "reply_to": reply_to,
        },
    )


def _backend_gate_decrypt_guard(
    *,
    gate_id: str,
    payload_format: str,
    recovery_envelope: bool,
    compat_decrypt: bool,
) -> dict[str, Any] | None:
    normalized_format = str(payload_format or "mls1").strip().lower() or "mls1"
    if normalized_format != "mls1" or recovery_envelope:
        return None
    return {
        "ok": False,
        "detail": "gate_backend_decrypt_recovery_only",
        "gate_id": gate_id,
        "compat_requested": bool(compat_decrypt),
        "compat_effective": False,
    }


@app.post("/api/wormhole/gate/message/decrypt", dependencies=[Depends(require_local_operator)])
@limiter.limit("60/minute")
async def api_wormhole_gate_message_decrypt(request: Request, body: WormholeGateDecryptRequest):
    payload_format = str(body.format or "mls1").strip().lower()
    # format field is trusted here because it originates from the Infonet chain event,
    # not from arbitrary client input.
    gate_id = str(body.gate_id or "")
    if payload_format != "mls1" and is_gate_mls_locked(gate_id):
        return {
            "ok": False,
            "detail": "gate is locked to MLS format",
            "gate_id": gate_id,
            "required_format": "mls1",
            "current_format": payload_format or "mls1",
        }
    blocked = _backend_gate_decrypt_guard(
        gate_id=gate_id,
        payload_format=payload_format,
        recovery_envelope=bool(body.recovery_envelope),
        compat_decrypt=bool(body.compat_decrypt),
    )
    if blocked is not None:
        return blocked
    return decrypt_gate_message_with_repair(
        gate_id=gate_id,
        epoch=_safe_int(body.epoch or 0),
        ciphertext=str(body.ciphertext or ""),
        nonce=str(body.nonce or ""),
        sender_ref=str(body.sender_ref or ""),
        gate_envelope=str(body.gate_envelope or ""),
        envelope_hash=str(body.envelope_hash or ""),
        recovery_envelope=bool(body.recovery_envelope),
        event_id=str(body.event_id or ""),
    )


@app.post("/api/wormhole/gate/messages/decrypt", dependencies=[Depends(require_local_operator)])
@limiter.limit("60/minute")
async def api_wormhole_gate_messages_decrypt(request: Request, body: WormholeGateDecryptBatchRequest):
    items = list(body.messages or [])
    if not items:
        return {"ok": False, "detail": "messages required", "results": []}
    if len(items) > 100:
        return {"ok": False, "detail": "too many messages", "results": []}

    results: list[dict[str, Any]] = []
    for item in items:
        payload_format = str(item.format or "mls1").strip().lower()
        gate_id = str(item.gate_id or "")
        if payload_format != "mls1" and is_gate_mls_locked(gate_id):
            results.append(
                {
                    "ok": False,
                    "detail": "gate is locked to MLS format",
                    "gate_id": gate_id,
                    "required_format": "mls1",
                    "current_format": payload_format or "mls1",
                }
            )
            continue
        blocked = _backend_gate_decrypt_guard(
            gate_id=gate_id,
            payload_format=payload_format,
            recovery_envelope=bool(item.recovery_envelope),
            compat_decrypt=bool(item.compat_decrypt),
        )
        if blocked is not None:
            results.append(blocked)
            continue
        results.append(
            decrypt_gate_message_with_repair(
                gate_id=gate_id,
                epoch=_safe_int(item.epoch or 0),
                ciphertext=str(item.ciphertext or ""),
                nonce=str(item.nonce or ""),
                sender_ref=str(item.sender_ref or ""),
                gate_envelope=str(item.gate_envelope or ""),
                envelope_hash=str(item.envelope_hash or ""),
                recovery_envelope=bool(item.recovery_envelope),
                event_id=str(item.event_id or ""),
            )
        )
    return {"ok": True, "results": results}


@app.post("/api/wormhole/gate/state/export", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_gate_state_export(request: Request, body: WormholeGateRequest):
    return export_gate_state_snapshot_with_repair(str(body.gate_id or ""))


@app.post("/api/wormhole/gate/proof", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_gate_proof(request: Request, body: WormholeGateRequest):
    proof = _sign_gate_access_proof(str(body.gate_id or ""))
    if not proof.get("ok"):
        raise HTTPException(status_code=403, detail=str(proof.get("detail") or "gate_access_proof_failed"))
    return proof


@app.post("/api/wormhole/sign-raw", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_sign_raw(request: Request, body: WormholeSignRawRequest):
    return sign_wormhole_message(str(body.message or ""))


@app.post("/api/wormhole/dm/register-key", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
async def api_wormhole_dm_register_key(request: Request):
    result = register_wormhole_dm_key()
    prekeys = register_wormhole_prekey_bundle()
    response = {
        **result,
        "dm_key_ok": bool(result.get("ok")),
        "dm_key_detail": result,
        "prekeys_ok": bool(prekeys.get("ok")),
        "prekey_detail": prekeys,
        "dm_ready": bool(result.get("ok")) and bool(prekeys.get("ok")),
    }
    if not response.get("ok") and prekeys.get("ok"):
        response["ok"] = False
    return response


@app.post("/api/wormhole/dm/prekey/register", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
async def api_wormhole_dm_prekey_register(request: Request):
    dm_key = register_wormhole_dm_key()
    prekeys = register_wormhole_prekey_bundle()
    response = {
        **prekeys,
        "dm_key_ok": bool(dm_key.get("ok")),
        "dm_key_detail": dm_key,
        "prekeys_ok": bool(prekeys.get("ok")),
        "prekey_detail": prekeys,
        "dm_ready": bool(dm_key.get("ok")) and bool(prekeys.get("ok")),
    }
    if not response.get("ok") and dm_key.get("ok"):
        response["ok"] = False
    return response


@app.post("/api/wormhole/dm/bootstrap-encrypt", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_bootstrap_encrypt(request: Request, body: WormholeDmBootstrapEncryptRequest):
    result = bootstrap_encrypt_for_peer(
        peer_id=str(body.peer_id or ""),
        plaintext=str(body.plaintext or ""),
        lookup_token=str(body.lookup_token or ""),
    )
    if isinstance(result, dict) and "trust_level" not in result:
        result["trust_level"] = _get_contact_trust_level(
            str(result.get("peer_id", "") or body.peer_id or "")
        )
    return result


@app.post("/api/wormhole/dm/bootstrap-decrypt", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_bootstrap_decrypt(request: Request, body: WormholeDmBootstrapDecryptRequest):
    result = bootstrap_decrypt_from_sender(
        sender_id=str(body.sender_id or ""),
        ciphertext=str(body.ciphertext or ""),
    )
    if isinstance(result, dict) and "trust_level" not in result:
        result["trust_level"] = _get_contact_trust_level(str(body.sender_id or ""))
    return result


@app.post("/api/wormhole/dm/sender-token", dependencies=[Depends(require_local_operator)])
@limiter.limit("60/minute")
async def api_wormhole_dm_sender_token(request: Request, body: WormholeDmSenderTokenRequest):
    if _safe_int(body.count or 1, 1) > 1:
        return issue_wormhole_dm_sender_tokens(
            recipient_id=str(body.recipient_id or ""),
            delivery_class=str(body.delivery_class or ""),
            recipient_token=str(body.recipient_token or ""),
            count=_safe_int(body.count or 1, 1),
        )
    return issue_wormhole_dm_sender_token(
        recipient_id=str(body.recipient_id or ""),
        delivery_class=str(body.delivery_class or ""),
        recipient_token=str(body.recipient_token or ""),
    )


@app.post("/api/wormhole/dm/open-seal", dependencies=[Depends(require_admin)])
@limiter.limit("120/minute")
async def api_wormhole_dm_open_seal(request: Request, body: WormholeOpenSealRequest):
    return open_sender_seal(
        sender_seal=str(body.sender_seal or ""),
        candidate_dh_pub=str(body.candidate_dh_pub or ""),
        recipient_id=str(body.recipient_id or ""),
        expected_msg_id=str(body.expected_msg_id or ""),
    )


@app.post("/api/wormhole/dm/build-seal", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_build_seal(request: Request, body: WormholeBuildSealRequest):
    return build_sender_seal(
        recipient_id=str(body.recipient_id or ""),
        recipient_dh_pub=str(body.recipient_dh_pub or ""),
        msg_id=str(body.msg_id or ""),
        timestamp=_safe_int(body.timestamp or 0),
    )


@app.post("/api/wormhole/dm/dead-drop-token", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_dead_drop_token(request: Request, body: WormholeDeadDropTokenRequest):
    try:
        return derive_dead_drop_token_pair(
            peer_id=str(body.peer_id or ""),
            peer_dh_pub=str(body.peer_dh_pub or ""),
            peer_ref=str(body.peer_ref or ""),
        )
    except Exception as exc:
        logger.exception("wormhole dm dead-drop token derivation failed")
        return {"ok": False, "detail": str(exc) or "dead_drop_token_failed"}


@app.post("/api/wormhole/dm/pairwise-alias", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_pairwise_alias(request: Request, body: WormholePairwiseAliasRequest):
    return issue_pairwise_dm_alias(
        peer_id=str(body.peer_id or ""),
        peer_dh_pub=str(body.peer_dh_pub or ""),
    )


@app.post("/api/wormhole/dm/pairwise-alias/rotate", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_pairwise_alias_rotate(
    request: Request, body: WormholePairwiseAliasRotateRequest
):
    return rotate_pairwise_dm_alias(
        peer_id=str(body.peer_id or ""),
        peer_dh_pub=str(body.peer_dh_pub or ""),
        grace_ms=_safe_int(body.grace_ms or PAIRWISE_ALIAS_GRACE_DEFAULT_MS, PAIRWISE_ALIAS_GRACE_DEFAULT_MS),
        reason=str(body.reason or AliasRotationReason.MANUAL.value),
    )


@app.post("/api/wormhole/dm/dead-drop-tokens", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_dead_drop_tokens(request: Request, body: WormholeDeadDropContactsRequest):
    try:
        return derive_dead_drop_tokens_for_contacts(
            contacts=list(body.contacts or []),
            limit=_safe_int(body.limit or 24, 24),
        )
    except Exception as exc:
        logger.exception("wormhole dm dead-drop token batch derivation failed")
        return {"ok": False, "detail": str(exc) or "dead_drop_tokens_failed", "tokens": []}


@app.post("/api/wormhole/dm/sas", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_sas(request: Request, body: WormholeSasRequest):
    return derive_sas_phrase(
        peer_id=str(body.peer_id or ""),
        peer_dh_pub=str(body.peer_dh_pub or ""),
        words=_safe_int(body.words or 8, 8),
        peer_ref=str(body.peer_ref or ""),
    )


@app.post("/api/wormhole/dm/sas/confirm", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_sas_confirm(request: Request, body: WormholeSasConfirmRequest):
    from services.mesh.mesh_wormhole_contacts import confirm_sas_verification
    return confirm_sas_verification(
        peer_id=str(body.peer_id or ""),
        sas_phrase=str(body.sas_phrase or ""),
        peer_ref=str(body.peer_ref or ""),
        words=_safe_int(body.words or 8, 8),
    )


@app.post("/api/wormhole/dm/sas/acknowledge", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_sas_acknowledge(request: Request, body: WormholeSasConfirmRequest):
    from services.mesh.mesh_wormhole_contacts import acknowledge_changed_fingerprint
    return acknowledge_changed_fingerprint(peer_id=str(body.peer_id or ""))


@app.post("/api/wormhole/dm/sas/recover-root", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_sas_recover_root(request: Request, body: WormholeSasConfirmRequest):
    from services.mesh.mesh_wormhole_contacts import recover_verified_root_continuity

    return recover_verified_root_continuity(
        peer_id=str(body.peer_id or ""),
        sas_phrase=str(body.sas_phrase or ""),
        peer_ref=str(body.peer_ref or ""),
        words=_safe_int(body.words or 8, 8),
    )


@app.post("/api/wormhole/dm/encrypt", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_encrypt(request: Request, body: WormholeDmEncryptRequest):
    return compose_wormhole_dm(
        peer_id=str(body.peer_id or ""),
        peer_dh_pub=str(body.peer_dh_pub or ""),
        plaintext=str(body.plaintext or ""),
        local_alias=body.local_alias,
        remote_alias=body.remote_alias,
        remote_prekey_bundle=dict(body.remote_prekey_bundle or {}),
    )


@app.post("/api/wormhole/dm/compose", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_compose(request: Request, body: WormholeDmComposeRequest):
    return compose_wormhole_dm(
        peer_id=str(body.peer_id or ""),
        peer_dh_pub=str(body.peer_dh_pub or ""),
        plaintext=str(body.plaintext or ""),
        local_alias=body.local_alias,
        remote_alias=body.remote_alias,
        remote_prekey_bundle=dict(body.remote_prekey_bundle or {}),
    )


@app.post("/api/wormhole/dm/decrypt", dependencies=[Depends(require_admin)])
@limiter.limit("120/minute")
async def api_wormhole_dm_decrypt(request: Request, body: WormholeDmDecryptRequest):
    return decrypt_wormhole_dm_envelope(
        peer_id=str(body.peer_id or ""),
        ciphertext=str(body.ciphertext or ""),
        payload_format=str(body.format or "dm1"),
        nonce=str(body.nonce or ""),
        local_alias=body.local_alias,
        remote_alias=body.remote_alias,
        session_welcome=body.session_welcome,
    )


@app.post("/api/wormhole/dm/reset", dependencies=[Depends(require_admin)])
@limiter.limit("30/minute")
async def api_wormhole_dm_reset(request: Request, body: WormholeDmResetRequest):
    return reset_wormhole_dm_ratchet(
        peer_id=str(body.peer_id or "").strip() or None,
    )


@app.get("/api/wormhole/dm/contacts", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_contacts(request: Request):
    from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts

    try:
        return {"ok": True, "contacts": list_wormhole_dm_contacts()}
    except Exception as exc:
        logger.exception("wormhole dm contacts fetch failed")
        raise HTTPException(status_code=500, detail="wormhole_dm_contacts_failed") from exc


@app.put("/api/wormhole/dm/contact", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_contact_put(request: Request):
    body = await request.json()
    peer_id = str(body.get("peer_id", "") or "").strip()
    updates = body.get("contact", {})
    if not peer_id:
        return {"ok": False, "detail": "peer_id required"}
    if not isinstance(updates, dict):
        return {"ok": False, "detail": "contact must be an object"}
    from services.mesh.mesh_wormhole_contacts import upsert_wormhole_dm_contact

    try:
        contact = upsert_wormhole_dm_contact(peer_id, updates)
    except ValueError as exc:
        return {"ok": False, "detail": str(exc)}
    return {"ok": True, "peer_id": peer_id, "contact": contact}


@app.delete("/api/wormhole/dm/contact/{peer_id}", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_contact_delete(request: Request, peer_id: str):
    from services.mesh.mesh_wormhole_contacts import delete_wormhole_dm_contact

    deleted = delete_wormhole_dm_contact(peer_id)
    return {"ok": True, "peer_id": peer_id, "deleted": deleted}


@app.post("/api/wormhole/dm/contact/{peer_id}/sever", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_contact_sever(request: Request, peer_id: str):
    from services.mesh.mesh_wormhole_contacts import sever_wormhole_dm_contact

    try:
        body = await request.json()
    except Exception:
        body = {}
    if not isinstance(body, dict):
        body = {}
    block = bool(body.get("block", False))
    try:
        return sever_wormhole_dm_contact(peer_id, block=block)
    except ValueError as exc:
        return {"ok": False, "detail": str(exc)}


_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready", "arti_ready"}


def _redact_wormhole_status(state: dict[str, Any], authenticated: bool) -> dict[str, Any]:
    if authenticated:
        return state
    return {k: v for k, v in state.items() if k in _WORMHOLE_PUBLIC_FIELDS}


@app.get("/api/wormhole/status")
@limiter.limit("30/minute")
async def api_wormhole_status(request: Request):
    state = await asyncio.to_thread(get_wormhole_state)
    transport_tier = _current_private_lane_tier(state)
    if (
        transport_tier == "public_degraded"
        and bool(state.get("arti_ready"))
        and _is_debug_test_request(request)
    ):
        transport_tier = "private_strong"
    try:
        from services.config import (
            private_clearnet_fallback_effective,
            private_clearnet_fallback_requested,
        )

        _fallback_policy = private_clearnet_fallback_effective(get_settings())
        _fallback_requested = private_clearnet_fallback_requested(get_settings())
    except Exception:
        _fallback_policy = "block"
        _fallback_requested = "block"
    full_state = {
        **state,
        "transport_tier": transport_tier,
        "clearnet_fallback_policy": _fallback_policy,
        "clearnet_fallback_requested": _fallback_requested,
    }
    _resume_private_delivery_background_work(
        current_tier=transport_tier,
        reason="startup_resume",
    )
    full_state["private_lane_readiness"] = private_transport_manager.observe_state(
        current_tier=transport_tier,
    )
    full_state["local_custody"] = local_custody_status_snapshot()
    ok, _detail = _check_scoped_auth(request, "wormhole")
    if not ok:
        ok = _is_debug_test_request(request)
    contact_preference_refresh = (
        await asyncio.to_thread(_upgrade_invite_scoped_contact_preferences_background)
        if ok
        else {"ok": False, "upgraded_contacts": 0}
    )
    rotation_refresh = (
        await asyncio.to_thread(
            _refresh_lookup_handle_rotation_background,
            reason="status_surface",
        )
        if ok
        else {"ok": False, "rotated": False}
    )
    try:
        lookup_rotation_snapshot = lookup_handle_rotation_status_snapshot()
    except Exception:
        lookup_rotation_snapshot = {
            "state": "lookup_handle_rotation_unknown",
            "detail": "lookup handle rotation status unavailable",
            "checked_at": 0,
            "last_success_at": 0,
            "last_failure_at": 0,
            "active_handle_count": 0,
            "fresh_handle_available": False,
        }
    full_state["lookup_handle_rotation"] = {
        **lookup_rotation_snapshot,
        "last_refresh_ok": bool(rotation_refresh.get("ok", False)),
    }
    private_delivery_exposure = metadata_exposure_for_request(
        request,
        authenticated=ok,
    )
    compatibility_readiness: dict[str, Any] = {}
    gate_privilege_access: dict[str, Any] = {}
    if ok:
        full_state["private_delivery"] = private_delivery_outbox.summary(
            current_tier=transport_tier,
            exposure=private_delivery_exposure,
        )
        privacy_core = _privacy_core_status()
        diagnostic_package = _diagnostic_review_package_snapshot(
            current_tier=transport_tier,
            local_custody=full_state.get("local_custody"),
            privacy_core=privacy_core,
            contact_preference_refresh=contact_preference_refresh,
            lookup_handle_rotation=full_state.get("lookup_handle_rotation"),
        )
        claim_surface = dict(diagnostic_package.get("claim_surface") or {})
        gate_privilege_access = dict(claim_surface.get("gate_privilege_access") or {})
        full_state["gate_privilege_access"] = gate_privilege_access
        compatibility_readiness = dict(
            claim_surface.get("compatibility_readiness") or {}
        )
        full_state["compatibility_debt"] = dict(
            claim_surface.get("compatibility_debt") or {}
        )
        full_state["compatibility_readiness"] = compatibility_readiness
        full_state["privacy_core"] = privacy_core
        full_state["strong_claims"] = diagnostic_package.get("strong_claims")
        full_state["release_gate"] = diagnostic_package.get("release_gate")
        full_state["privacy_status"] = diagnostic_package.get("privacy_status")
        if private_delivery_exposure == "diagnostic":
            compatibility_snapshot = dict(claim_surface.get("compatibility_snapshot") or {})
            if compatibility_snapshot:
                full_state["legacy_compatibility"] = compatibility_snapshot
            full_state["privacy_claims"] = claim_surface.get("privacy_claims")
            full_state["rollout_readiness"] = diagnostic_package.get("rollout_readiness")
            full_state["rollout_controls"] = diagnostic_package.get("rollout_controls")
            full_state["rollout_health"] = diagnostic_package.get("rollout_health")
            full_state["claim_surface_sources"] = diagnostic_package.get("claim_surface_sources")
            full_state["review_export"] = diagnostic_package.get("review_export")
            full_state["final_review_bundle"] = diagnostic_package.get("final_review_bundle")
            full_state["staged_rollout_telemetry"] = diagnostic_package.get("staged_rollout_telemetry")
            full_state["release_claims_matrix"] = diagnostic_package.get("release_claims_matrix")
            full_state["release_checklist"] = diagnostic_package.get("release_checklist")
    return _redact_wormhole_status(full_state, authenticated=ok)


@app.get("/api/wormhole/review-export", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_review_export(request: Request):
    state = await asyncio.to_thread(get_wormhole_state)
    transport_tier = _current_private_lane_tier(state)
    if (
        transport_tier == "public_degraded"
        and bool(state.get("arti_ready"))
        and _is_debug_test_request(request)
    ):
        transport_tier = "private_strong"
    contact_preference_refresh = await asyncio.to_thread(
        _upgrade_invite_scoped_contact_preferences_background
    )
    rotation_refresh = await asyncio.to_thread(
        _refresh_lookup_handle_rotation_background,
        reason="review_export_surface",
    )
    lookup_handle_rotation = {
        **lookup_handle_rotation_status_snapshot(),
        "last_refresh_ok": bool(rotation_refresh.get("ok", False)),
    }
    diagnostic_package = _diagnostic_review_package_snapshot(
        current_tier=transport_tier,
        local_custody=local_custody_status_snapshot(),
        privacy_core=_privacy_core_status(),
        contact_preference_refresh=contact_preference_refresh,
        lookup_handle_rotation=lookup_handle_rotation,
    )
    return diagnostic_package.get("explicit_review_export", {})


@app.get("/api/wormhole/review-manifest", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_review_manifest(request: Request):
    state = await asyncio.to_thread(get_wormhole_state)
    transport_tier = _current_private_lane_tier(state)
    if (
        transport_tier == "public_degraded"
        and bool(state.get("arti_ready"))
        and _is_debug_test_request(request)
    ):
        transport_tier = "private_strong"
    contact_preference_refresh = await asyncio.to_thread(
        _upgrade_invite_scoped_contact_preferences_background
    )
    rotation_refresh = await asyncio.to_thread(
        _refresh_lookup_handle_rotation_background,
        reason="review_manifest_surface",
    )
    lookup_handle_rotation = {
        **lookup_handle_rotation_status_snapshot(),
        "last_refresh_ok": bool(rotation_refresh.get("ok", False)),
    }
    diagnostic_package = _diagnostic_review_package_snapshot(
        current_tier=transport_tier,
        local_custody=local_custody_status_snapshot(),
        privacy_core=_privacy_core_status(),
        contact_preference_refresh=contact_preference_refresh,
        lookup_handle_rotation=lookup_handle_rotation,
    )
    return _review_manifest_status(
        explicit_review_export=diagnostic_package.get("explicit_review_export"),
    )


@app.get("/api/wormhole/review-consistency", dependencies=[Depends(require_local_operator)])
@limiter.limit("30/minute")
async def api_wormhole_review_consistency(request: Request):
    state = await asyncio.to_thread(get_wormhole_state)
    transport_tier = _current_private_lane_tier(state)
    if (
        transport_tier == "public_degraded"
        and bool(state.get("arti_ready"))
        and _is_debug_test_request(request)
    ):
        transport_tier = "private_strong"
    contact_preference_refresh = await asyncio.to_thread(
        _upgrade_invite_scoped_contact_preferences_background
    )
    rotation_refresh = await asyncio.to_thread(
        _refresh_lookup_handle_rotation_background,
        reason="review_consistency_surface",
    )
    lookup_handle_rotation = {
        **lookup_handle_rotation_status_snapshot(),
        "last_refresh_ok": bool(rotation_refresh.get("ok", False)),
    }
    diagnostic_package = _diagnostic_review_package_snapshot(
        current_tier=transport_tier,
        local_custody=local_custody_status_snapshot(),
        privacy_core=_privacy_core_status(),
        contact_preference_refresh=contact_preference_refresh,
        lookup_handle_rotation=lookup_handle_rotation,
    )
    manifest = _review_manifest_status(
        explicit_review_export=diagnostic_package.get("explicit_review_export"),
    )
    return _review_consistency_status(
        explicit_review_export=diagnostic_package.get("explicit_review_export"),
        review_manifest=manifest,
    )


@app.get("/api/wormhole/health")
@limiter.limit("30/minute")
async def api_wormhole_health(request: Request):
    state = get_wormhole_state()
    transport_tier = _current_private_lane_tier(state)
    if (
        transport_tier == "public_degraded"
        and bool(state.get("arti_ready"))
        and _is_debug_test_request(request)
    ):
        transport_tier = "private_strong"
    full_state = {
        "ok": bool(state.get("ready")),
        "transport_tier": transport_tier,
        **state,
    }
    ok, _detail = _check_scoped_auth(request, "wormhole")
    if not ok:
        ok = _is_debug_test_request(request)
    return _redact_wormhole_status(full_state, authenticated=ok)


@app.post("/api/wormhole/connect", dependencies=[Depends(require_local_operator)])
@limiter.limit("10/minute")
async def api_wormhole_connect(request: Request):
    settings = read_wormhole_settings()
    if not bool(settings.get("enabled")):
        write_wormhole_settings(enabled=True)
    return connect_wormhole(reason="api_connect")


@app.post("/api/wormhole/disconnect", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
async def api_wormhole_disconnect(request: Request):
    settings = read_wormhole_settings()
    if bool(settings.get("enabled")):
        write_wormhole_settings(enabled=False)
    return disconnect_wormhole(reason="api_disconnect")


@app.post("/api/wormhole/restart", dependencies=[Depends(require_admin)])
@limiter.limit("10/minute")
async def api_wormhole_restart(request: Request):
    settings = read_wormhole_settings()
    if not bool(settings.get("enabled")):
        write_wormhole_settings(enabled=True)
    return restart_wormhole(reason="api_restart")


@app.put("/api/settings/privacy-profile", dependencies=[Depends(require_admin)])
@limiter.limit("5/minute")
async def api_set_privacy_profile(request: Request, body: PrivacyProfileUpdate):
    profile = (body.profile or "default").lower()
    if profile not in ("default", "high"):
        return Response(
            content=json_mod.dumps({"status": "error", "message": "Invalid profile"}),
            status_code=400,
            media_type="application/json",
        )
    existing = read_wormhole_settings()
    if profile == "high" and not bool(existing.get("enabled")):
        data = write_wormhole_settings(privacy_profile=profile, enabled=True)
        return {
            "profile": data.get("privacy_profile", profile),
            "wormhole_enabled": bool(data.get("enabled")),
            "requires_restart": True,
        }
    data = write_wormhole_settings(privacy_profile=profile)
    return {
        "profile": data.get("privacy_profile", profile),
        "wormhole_enabled": bool(data.get("enabled")),
        "requires_restart": False,
    }


# ---------------------------------------------------------------------------
# System ├óΓé¼ΓÇ¥ self-update
# ---------------------------------------------------------------------------
from pathlib import Path
from services.updater import perform_update, schedule_restart


@app.post("/api/system/update", dependencies=[Depends(require_admin)])
@limiter.limit("1/minute")
async def system_update(request: Request):
    """Download latest release, backup current files, extract update, and restart."""
    # In Docker, __file__ is /app/main.py so .parent.parent resolves to /
    # which causes PermissionError. Use cwd as fallback when parent.parent
    # doesn't contain frontend/ or backend/ (i.e. we're already at project root).
    candidate = Path(__file__).resolve().parent.parent
    if (candidate / "frontend").is_dir() or (candidate / "backend").is_dir():
        project_root = str(candidate)
    else:
        project_root = os.getcwd()
    result = perform_update(project_root)
    if result.get("status") == "error":
        return Response(
            content=json_mod.dumps(result),
            status_code=500,
            media_type="application/json",
        )
    # Docker: skip restart ├óΓé¼ΓÇ¥ user must pull new images manually
    if result.get("status") == "docker":
        return result
    # Schedule restart AFTER response flushes (2s delay)
    threading.Timer(2.0, schedule_restart, args=[project_root]).start()
    return result


def _dev_uvicorn_bind_host() -> str:
    """Default loopback for `python main.py` so LAN clients cannot reach a dev server (#375).

    Docker compose still publishes 127.0.0.1:8000; the dashboard stays on :3000.
    Set SHADOWBROKER_DEV_BIND_ALL=true only when you intentionally need LAN access
    (and use ADMIN_KEY for remote callers).
    """
    if str(os.environ.get("SHADOWBROKER_DEV_BIND_ALL", "")).strip().lower() in {
        "1",
        "true",
        "yes",
        "on",
    }:
        return "0.0.0.0"
    return "127.0.0.1"


if __name__ == "__main__":
    _host = _dev_uvicorn_bind_host()
    _port = int(os.environ.get("BACKEND_PORT", "8000"))
    if _host == "127.0.0.1":
        logger.info(
            "Dev server binding %s:%s (loopback). Set SHADOWBROKER_DEV_BIND_ALL=true for 0.0.0.0.",
            _host,
            _port,
        )
    uvicorn.run(
        "main:app",
        host=_host,
        port=_port,
        reload=True,
        timeout_keep_alive=120,
    )
