// frontend/src/pages/developer/ApiReferencePage.jsx
// Vesper Firmware API Reference v2 — Command explorer

import { useState, useMemo } from 'react'
import PageHeader from '@/components/ui/PageHeader'
import Card from '@/components/ui/Card'
import SearchBar from '@/components/ui/SearchBar'
import Tabs from '@/components/ui/Tabs'

// ---------------------------------------------------------------------------
// Data
// ---------------------------------------------------------------------------

const TRANSPORTS = [
  { transport: 'MQTT', address: 'vesper/{device_id}/control', direction: 'Inbound', notes: 'Commands in. Responses go to vesper/{device_id}/data' },
  { transport: 'MQTT', address: 'vesper/{device_id}/data', direction: 'Outbound', notes: 'Responses + events' },
  { transport: 'MQTT', address: 'vesper/{device_id}/status/heartbeat', direction: 'Outbound, retained', notes: 'Heartbeat every 30 s' },
  { transport: 'MQTT', address: 'vesper/{device_id}/status/alerts', direction: 'Outbound, QoS 1', notes: 'Subsystem state changes (WARNING / CRITICAL / FAILED / CLEARED). Published on transition only.' },
  { transport: 'MQTT', address: 'vesper/{device_id}/status/info', direction: 'Outbound, QoS 0', notes: 'Significant device events (playback start/stop, etc.).' },
  { transport: 'WebSocket', address: 'ws://{device_ip}/ws', direction: 'Bidirectional', notes: 'Requires identify on connect' },
  { transport: 'HTTP', address: 'http://{device_ip}/...', direction: 'REST', notes: 'Web console endpoints' },
  { transport: 'UART', address: 'Hardware serial', direction: 'Bidirectional', notes: 'Restricted — whitelist only' },
  { transport: 'UDP', address: 'Port 32101', direction: 'Discovery', notes: 'Passive — device announces itself' },
]

const UART_WHITELIST = ['ping', 'playback', 'clock.pause', 'clock.resume', 'system.get_time']

const NAMESPACES = [
  {
    id: 'root',
    label: 'Root',
    description: 'Top-level commands — no namespace prefix.',
    commands: [
      { cmd: 'ping', handler: 'SystemHandler', transports: ['All'], description: 'Connectivity check. No contents required.', contents: null, returns: '{ "status": "SUCCESS", "type": "pong" }', example: '{ "v": 2, "cmd": "ping" }', warning: null },
      { cmd: 'identify', handler: 'SystemHandler', transports: ['WebSocket'], description: 'Register client type with the device. Must be sent immediately after connecting on WebSocket or targeted responses will not be received.', contents: [{ field: 'device_type', type: 'string', required: true, notes: '"master" (app/console) or "secondary" (slave board)' }], returns: null, example: '{ "v": 2, "cmd": "identify", "contents": { "device_type": "master" } }', warning: null },
      { cmd: 'playback', handler: 'PlaybackHandler', transports: ['All'], description: 'Control melody playback. The action field is always required.', contents: [{ field: 'action', type: 'string', required: true, notes: '"play" | "stop" | "pause" | "unpause"' }, { field: 'melody_uid', type: 'string', required: false, notes: 'Required when action is play' }], returns: null, example: '{ "v": 2, "cmd": "playback", "contents": { "action": "play", "melody_uid": "ABC123" } }\n{ "v": 2, "cmd": "playback", "contents": { "action": "stop" } }', warning: null },
    ],
  },
  {
    id: 'relay',
    label: 'relay',
    description: 'Bell relay channel configuration and testing. Handler: RelayHandler.',
    commands: [
      { cmd: 'relay.set_durations', handler: 'RelayHandler', transports: ['All'], description: 'Set strike duration (ms) for one or more bell channels. Partial updates accepted — omitted channels are unchanged.', contents: [{ field: 'bells', type: 'object', required: true, notes: 'Map of channel index → duration ms. e.g. { "0": 95, "2": 110 }. At least one entry required.' }], returns: null, example: '{\n  "v": 2,\n  "cmd": "relay.set_durations",\n  "contents": { "bells": { "0": 95, "1": 100, "2": 110 } }\n}', warning: null },
      { cmd: 'relay.set_outputs', handler: 'RelayHandler', transports: ['All'], description: 'Map bell channels to physical relay outputs. Partial updates accepted.', contents: [{ field: 'bells', type: 'object', required: true, notes: 'Map of channel index → relay output index. e.g. { "0": 2, "1": 3 }' }], returns: null, example: '{\n  "v": 2,\n  "cmd": "relay.set_outputs",\n  "contents": { "bells": { "0": 2, "1": 3, "2": 4 } }\n}', warning: null },
      { cmd: 'relay.get_config', handler: 'RelayHandler', transports: ['All'], description: 'Read current bell durations and relay output assignments.', contents: null, returns: '{ "durations": { "0": 95, ... }, "outputs": { "0": 2, ... } }', example: '{ "v": 2, "cmd": "relay.get_config" }', warning: null },
      { cmd: 'relay.test_output', handler: 'RelayHandler', transports: ['All'], description: 'Fire a specific relay for a caller-specified duration. Safety-gated: Player must be stopped.', contents: [{ field: 'output', type: 'int', required: true, notes: 'Physical relay output index' }, { field: 'duration_ms', type: 'int', required: true, notes: 'Duration in ms. No defaults. Typical range: 85–140 ms' }], returns: null, example: '{\n  "v": 2,\n  "cmd": "relay.test_output",\n  "contents": { "output": 2, "duration_ms": 95 }\n}', warning: null },
    ],
  },
  {
    id: 'clock',
    label: 'clock',
    description: 'Clock strike configuration, RTC time, face position, timezone, and silence periods. Handler: ClockHandler.',
    commands: [
      { cmd: 'clock.set_outputs', handler: 'ClockHandler', transports: ['All'], description: 'Set which relay outputs the clock strikes use. Partial updates accepted.', contents: [{ field: 'c1', type: 'int', required: false, notes: 'Relay output for the first clock channel' }, { field: 'c2', type: 'int', required: false, notes: 'Relay output for the second clock channel. At least one of c1/c2 required.' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_outputs", "contents": { "c1": 4, "c2": 5 } }', warning: null },
      { cmd: 'clock.set_timings', handler: 'ClockHandler', transports: ['All'], description: 'Set clock strike timing configuration. Partial updates accepted.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.set_timings", "contents": { ... } }', warning: null },
      { cmd: 'clock.set_alerts', handler: 'ClockHandler', transports: ['All'], description: 'Configure hourly/quarter alert behavior. Partial updates accepted.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.set_alerts", "contents": { ... } }', warning: null },
      { cmd: 'clock.set_backlight', handler: 'ClockHandler', transports: ['All'], description: 'Configure LCD backlight behavior. Partial updates accepted.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.set_backlight", "contents": { ... } }', warning: null },
      { cmd: 'clock.set_silence', handler: 'ClockHandler', transports: ['All'], description: 'Configure silence period (quiet hours). Partial updates accepted.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.set_silence", "contents": { ... } }', warning: null },
      { cmd: 'clock.set_time', handler: 'ClockHandler', transports: ['All'], description: 'Set the RTC time.', contents: [{ field: 'timestamp', type: 'int', required: true, notes: 'Unix epoch timestamp' }, { field: 'timezone_offset', type: 'int', required: false, notes: 'Offset in seconds (e.g. 7200 for UTC+2)' }, { field: 'dst_offset', type: 'int', required: false, notes: 'DST offset in seconds' }], returns: null, example: '{\n  "v": 2,\n  "cmd": "clock.set_time",\n  "contents": { "timestamp": 1740000000, "timezone_offset": 7200 }\n}', warning: null },
      { cmd: 'clock.set_face', handler: 'ClockHandler', transports: ['All'], description: 'Set the physical analog clock face position.', contents: [{ field: 'hour', type: 'int', required: true, notes: 'Hour hand position (0–11)' }, { field: 'minute', type: 'int', required: true, notes: 'Minute hand position (0–59)' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_face", "contents": { "hour": 10, "minute": 10 } }', warning: null },
      { cmd: 'clock.set_timezone', handler: 'ClockHandler', transports: ['All'], description: 'Set timezone independently of RTC time.', contents: [{ field: 'gmt_offset_sec', type: 'int', required: true, notes: 'GMT offset in seconds (e.g. 7200 for UTC+2)' }, { field: 'dst_offset_sec', type: 'int', required: false, notes: 'DST offset in seconds' }, { field: 'timezone_name', type: 'string', required: false, notes: 'IANA timezone name, e.g. "Europe/Athens"' }], returns: null, example: '{\n  "v": 2,\n  "cmd": "clock.set_timezone",\n  "contents": { "gmt_offset_sec": 7200, "dst_offset_sec": 3600, "timezone_name": "Europe/Athens" }\n}', warning: null },
      { cmd: 'clock.get_timezone', handler: 'ClockHandler', transports: ['All'], description: 'Read current timezone settings.', contents: null, returns: '{ "gmt_offset_sec": 7200, "dst_offset_sec": 3600, "timezone_name": "..." }', example: '{ "v": 2, "cmd": "clock.get_timezone" }', warning: null },
      { cmd: 'clock.sync_ntp', handler: 'ClockHandler', transports: ['All'], description: 'Force an immediate NTP time sync.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.sync_ntp" }', warning: null },
      { cmd: 'clock.enable', handler: 'ClockHandler', transports: ['All'], description: 'Enable clock strike output.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.enable" }', warning: null },
      { cmd: 'clock.disable', handler: 'ClockHandler', transports: ['All'], description: 'Disable clock strike output.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.disable" }', warning: null },
      { cmd: 'clock.pause', handler: 'ClockHandler', transports: ['All'], description: 'Pause clock updates.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.pause" }', warning: null },
      { cmd: 'clock.resume', handler: 'ClockHandler', transports: ['All'], description: 'Resume clock updates.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.resume" }', warning: null },
      { cmd: 'clock.get_face', handler: 'ClockHandler', transports: ['All'], description: 'Read current analog clock face position.', contents: null, returns: '{ "clock_hour", "clock_minute", "last_sync_time", "next_output_is_c1" }', example: '{ "v": 2, "cmd": "clock.get_face" }', warning: null },
      { cmd: 'clock.get_config', handler: 'ClockHandler', transports: ['All'], description: 'Read full clock configuration — outputs, timings, alerts, backlight, silence, enabled state.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.get_config" }', warning: null },
    ],
  },
  {
    id: 'system',
    label: 'system',
    description: 'Device status, settings, health, and control. Handler: SystemHandler.',
    commands: [
      { cmd: 'system.status', handler: 'SystemHandler', transports: ['All'], description: 'Get current device status: player state, strike counters, projected run time.', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.status" }', warning: null },
      { cmd: 'system.get_time', handler: 'SystemHandler', transports: ['All'], description: 'Get current RTC time.', contents: null, returns: '{ "local_timestamp", "utc_timestamp", "year", "month", "day", "hour", "minute", "second", "rtc_available" }', example: '{ "v": 2, "cmd": "system.get_time" }', warning: null },
      { cmd: 'system.get_settings', handler: 'SystemHandler', transports: ['All'], description: 'Get full ConfigManager JSON dump (all saved settings).', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.get_settings" }', warning: null },
      { cmd: 'system.get_device_info', handler: 'SystemHandler', transports: ['All'], description: 'Get device identity and runtime stats.', contents: null, returns: '{ "uid", "hw_type", "hw_version", "fw_version", "uptime_ms", "free_heap", "min_free_heap" }', example: '{ "v": 2, "cmd": "system.get_device_info" }', warning: null },
      { cmd: 'system.health', handler: 'SystemHandler', transports: ['All'], description: 'Get full HealthMonitor report — all subsystem states, warnings, and critical failures.', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.health" }', warning: null },
      { cmd: 'system.factory_reset', handler: 'SystemHandler', transports: ['All'], description: 'Reset all settings to factory defaults.', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.factory_reset" }', warning: 'Non-reversible. All saved configuration is permanently wiped.' },
      { cmd: 'system.restart', handler: 'SystemHandler', transports: ['All'], description: 'Reboot the device. Response is sent before reboot (2 s delay).', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.restart" }', warning: null },
    ],
  },
  {
    id: 'firmware',
    label: 'firmware',
    description: 'OTA slot management — validate, commit, and roll back firmware. Handler: FirmwareHandler.',
    commands: [
      { cmd: 'firmware.status', handler: 'FirmwareHandler', transports: ['All'], description: 'Get firmware validation state.', contents: null, returns: '{ "validation_state", "version", "boot_count", "can_commit", "can_rollback" }', example: '{ "v": 2, "cmd": "firmware.status" }', warning: null },
      { cmd: 'firmware.commit', handler: 'FirmwareHandler', transports: ['All'], description: 'Commit the current firmware as permanent (marks OTA slot as valid).', contents: null, returns: null, example: '{ "v": 2, "cmd": "firmware.commit" }', warning: null },
      { cmd: 'firmware.rollback', handler: 'FirmwareHandler', transports: ['All'], description: 'Roll back to the previous firmware. Device reboots.', contents: null, returns: null, example: '{ "v": 2, "cmd": "firmware.rollback" }', warning: 'Device will reboot immediately after this command.' },
    ],
  },
  {
    id: 'ota',
    label: 'ota',
    description: 'Over-the-air update management. Handler: FirmwareHandler.',
    commands: [
      { cmd: 'ota.status', handler: 'FirmwareHandler', transports: ['All'], description: 'Get OTA update status.', contents: null, returns: '{ "current_version", "available_version", "channel", "update_available", "last_check", "progress" }', example: '{ "v": 2, "cmd": "ota.status" }', warning: null },
      { cmd: 'ota.check', handler: 'FirmwareHandler', transports: ['All'], description: 'Check for available updates.', contents: [{ field: 'channel', type: 'string', required: false, notes: 'Update channel. Default: "stable"' }], returns: null, example: '{ "v": 2, "cmd": "ota.check", "contents": { "channel": "beta" } }', warning: null },
      { cmd: 'ota.update', handler: 'FirmwareHandler', transports: ['All'], description: 'Trigger OTA update from VPS. Device may reboot after flashing.', contents: [{ field: 'channel', type: 'string', required: false, notes: 'Update channel. Default: "stable"' }], returns: null, example: '{ "v": 2, "cmd": "ota.update", "contents": { "channel": "stable" } }', warning: 'Device may reboot after the update is applied.' },
      { cmd: 'ota.custom', handler: 'FirmwareHandler', transports: ['All'], description: 'Flash firmware from a custom URL.', contents: [{ field: 'firmware_url', type: 'string', required: true, notes: 'Direct download URL for the firmware binary' }, { field: 'checksum', type: 'string', required: false, notes: 'Optional integrity checksum' }, { field: 'file_size', type: 'int', required: false, notes: 'Optional file size in bytes' }, { field: 'version', type: 'string', required: false, notes: 'Version label for display purposes' }], returns: null, example: '{\n  "v": 2,\n  "cmd": "ota.custom",\n  "contents": {\n    "firmware_url": "http://example.com/firmware.bin",\n    "checksum": "abc123",\n    "version": "142"\n  }\n}', warning: 'Device may reboot after the update is applied.' },
    ],
  },
  {
    id: 'network',
    label: 'network',
    description: 'Network configuration and connection status. Handler: NetworkHandler.',
    commands: [
      { cmd: 'network.info', handler: 'NetworkHandler', transports: ['All'], description: 'Get IP address, gateway, and DNS.', contents: null, returns: '{ "ip", "gateway", "dns" }', example: '{ "v": 2, "cmd": "network.info" }', warning: null },
      { cmd: 'network.status', handler: 'NetworkHandler', transports: ['All'], description: 'Get full connection state.', contents: null, returns: '{ "connected", "state", "type", "ip", "rssi", "ap_mode" }', example: '{ "v": 2, "cmd": "network.status" }', warning: null },
      { cmd: 'network.set_config', handler: 'NetworkHandler', transports: ['All'], description: 'Update network configuration. Partial updates accepted. Returns restart_required flag.', contents: [{ field: 'hostname', type: 'string', required: false, notes: 'Device hostname on the network' }, { field: 'useStaticIP', type: 'bool', required: false, notes: 'Enable static IP mode' }, { field: 'ip', type: 'string', required: false, notes: 'Static IP address. Required together with gateway + subnet when using static IP.' }, { field: 'gateway', type: 'string', required: false, notes: 'Gateway address' }, { field: 'subnet', type: 'string', required: false, notes: 'Subnet mask' }, { field: 'dns1', type: 'string', required: false, notes: 'Primary DNS' }, { field: 'dns2', type: 'string', required: false, notes: 'Secondary DNS' }], returns: '{ "restart_required": true/false }', example: '{\n  "v": 2,\n  "cmd": "network.set_config",\n  "contents": { "hostname": "vesper-lab", "useStaticIP": false }\n}', warning: null },
    ],
  },
  {
    id: 'files',
    label: 'files',
    description: 'Melody file management on the SD card and built-in firmware storage. Handler: FileHandler.',
    commands: [
      { cmd: 'files.list', handler: 'FileHandler', transports: ['All'], description: 'List melody files on the SD card.', contents: null, returns: 'Array of SD card melody file objects', example: '{ "v": 2, "cmd": "files.list" }', warning: null },
      { cmd: 'files.list_builtin', handler: 'FileHandler', transports: ['All'], description: 'List all built-in melodies compiled into the firmware.', contents: null, returns: '[{ "name": "...", "uid": "..." }, ...]', example: '{ "v": 2, "cmd": "files.list_builtin" }', warning: null },
      { cmd: 'files.download', handler: 'FileHandler', transports: ['All'], description: 'Download a melody from the server to the SD card.', contents: [{ field: 'melodys_uid', type: 'string', required: true, notes: 'UID of the melody to download' }], returns: null, example: '{ "v": 2, "cmd": "files.download", "contents": { "melodys_uid": "ABC123" } }', warning: null },
      { cmd: 'files.delete', handler: 'FileHandler', transports: ['All'], description: 'Delete a melody file from the SD card.', contents: [{ field: 'name', type: 'string', required: true, notes: 'Filename including extension, e.g. "westminster.mel"' }], returns: null, example: '{ "v": 2, "cmd": "files.delete", "contents": { "name": "westminster.mel" } }', warning: null },
    ],
  },
  {
    id: 'log',
    label: 'log',
    description: 'Log verbosity control per output channel. Level 0 = off, 5 = verbose. Handler: LoggingHandler.',
    commands: [
      { cmd: 'log.set_serial', handler: 'LoggingHandler', transports: ['All'], description: 'Set log verbosity for the UART serial output.', contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], returns: null, example: '{ "v": 2, "cmd": "log.set_serial", "contents": { "level": 3 } }', warning: null },
      { cmd: 'log.set_sd', handler: 'LoggingHandler', transports: ['All'], description: 'Set log verbosity for SD card logging.', contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], returns: null, example: '{ "v": 2, "cmd": "log.set_sd", "contents": { "level": 2 } }', warning: null },
      { cmd: 'log.set_mqtt', handler: 'LoggingHandler', transports: ['All'], description: 'Set log verbosity for MQTT log publishing.', contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], returns: null, example: '{ "v": 2, "cmd": "log.set_mqtt", "contents": { "level": 1 } }', warning: null },
    ],
  },
  {
    id: 'mqtt',
    label: 'mqtt',
    description: 'MQTT connectivity control. Handler: MQTTHandler.',
    commands: [
      { cmd: 'mqtt.enable', handler: 'MQTTHandler', transports: ['All'], description: 'Enable MQTT connectivity.', contents: null, returns: null, example: '{ "v": 2, "cmd": "mqtt.enable" }', warning: null },
      { cmd: 'mqtt.disable', handler: 'MQTTHandler', transports: ['All'], description: 'Disable MQTT connectivity.', contents: null, returns: null, example: '{ "v": 2, "cmd": "mqtt.disable" }', warning: null },
    ],
  },
  {
    id: 'telemetry',
    label: 'telemetry',
    description: 'Bell strike counters and thermal load monitoring. Handler: TelemetryHandler.',
    commands: [
      { cmd: 'telemetry.get_strikes', handler: 'TelemetryHandler', transports: ['All'], description: 'Get strike counts per bell channel.', contents: null, returns: '{ "strikes": { "0": 1234, "1": 567, ... } }', example: '{ "v": 2, "cmd": "telemetry.get_strikes" }', warning: null },
      { cmd: 'telemetry.reset_strikes', handler: 'TelemetryHandler', transports: ['All'], description: 'Reset strike counters. Omit bell to reset all channels; include it to reset a single channel.', contents: [{ field: 'bell', type: 'int', required: false, notes: 'Channel index. If omitted, all channels are reset.' }], returns: null, example: '{ "v": 2, "cmd": "telemetry.reset_strikes" }\n{ "v": 2, "cmd": "telemetry.reset_strikes", "contents": { "bell": 2 } }', warning: null },
      { cmd: 'telemetry.get_loads', handler: 'TelemetryHandler', transports: ['All'], description: 'Get heat load per bell channel, cooling status, and overload flags.', contents: null, returns: '{ "loads": { "0": 42.5, ... }, "cooling_active": false, "overloaded": [2, 5] }', example: '{ "v": 2, "cmd": "telemetry.get_loads" }', warning: null },
    ],
  },
]

const LEGACY_MAP = [
  { v1_cmd: 'ping', v1_action: '—', v2: 'ping' },
  { v1_cmd: 'identify', v1_action: '—', v2: 'identify' },
  { v1_cmd: 'playback', v1_action: '—', v2: 'playback' },
  { v1_cmd: 'file_manager', v1_action: 'list_melodies', v2: 'files.list' },
  { v1_cmd: 'file_manager', v1_action: 'download_melody', v2: 'files.download' },
  { v1_cmd: 'file_manager', v1_action: 'delete_melody', v2: 'files.delete' },
  { v1_cmd: 'relay_setup', v1_action: 'set_timings', v2: 'relay.set_durations' },
  { v1_cmd: 'relay_setup', v1_action: 'set_outputs', v2: 'relay.set_outputs' },
  { v1_cmd: 'clock_setup', v1_action: 'set_outputs', v2: 'clock.set_outputs' },
  { v1_cmd: 'clock_setup', v1_action: 'set_timings', v2: 'clock.set_timings' },
  { v1_cmd: 'clock_setup', v1_action: 'set_alerts', v2: 'clock.set_alerts' },
  { v1_cmd: 'clock_setup', v1_action: 'set_backlight', v2: 'clock.set_backlight' },
  { v1_cmd: 'clock_setup', v1_action: 'set_silence', v2: 'clock.set_silence' },
  { v1_cmd: 'clock_setup', v1_action: 'set_rtc_time', v2: 'clock.set_time' },
  { v1_cmd: 'clock_setup', v1_action: 'set_physical_clock_time', v2: 'clock.set_face' },
  { v1_cmd: 'clock_setup', v1_action: 'pause_clock_updates', v2: 'clock.pause' },
  { v1_cmd: 'clock_setup', v1_action: 'resume_clock_updates', v2: 'clock.resume' },
  { v1_cmd: 'clock_setup', v1_action: 'set_enabled (enabled=true)', v2: 'clock.enable' },
  { v1_cmd: 'clock_setup', v1_action: 'set_enabled (enabled=false)', v2: 'clock.disable' },
  { v1_cmd: 'system_info', v1_action: 'report_status', v2: 'system.status' },
  { v1_cmd: 'system_info', v1_action: 'get_device_time', v2: 'system.get_time' },
  { v1_cmd: 'system_info', v1_action: 'get_clock_time', v2: 'clock.get_face' },
  { v1_cmd: 'system_info', v1_action: 'get_firmware_status', v2: 'firmware.status' },
  { v1_cmd: 'system_info', v1_action: 'network_info', v2: 'network.info' },
  { v1_cmd: 'system_info', v1_action: 'get_full_settings', v2: 'system.get_settings' },
  { v1_cmd: 'system', v1_action: 'status', v2: 'system.status' },
  { v1_cmd: 'system', v1_action: 'reset_defaults', v2: 'system.factory_reset' },
  { v1_cmd: 'system', v1_action: 'commit_firmware', v2: 'firmware.commit' },
  { v1_cmd: 'system', v1_action: 'rollback_firmware', v2: 'firmware.rollback' },
  { v1_cmd: 'system', v1_action: 'get_firmware_status', v2: 'firmware.status' },
  { v1_cmd: 'system', v1_action: 'set_network_config', v2: 'network.set_config' },
  { v1_cmd: 'system', v1_action: 'set_serial_log_level', v2: 'log.set_serial' },
  { v1_cmd: 'system', v1_action: 'set_sd_log_level', v2: 'log.set_sd' },
  { v1_cmd: 'system', v1_action: 'set_mqtt_log_level', v2: 'log.set_mqtt' },
  { v1_cmd: 'system', v1_action: 'set_mqtt_enabled (enabled=true)', v2: 'mqtt.enable' },
  { v1_cmd: 'system', v1_action: 'set_mqtt_enabled (enabled=false)', v2: 'mqtt.disable' },
  { v1_cmd: 'system', v1_action: 'restart', v2: 'system.restart' },
  { v1_cmd: 'system', v1_action: 'reboot', v2: 'system.restart' },
  { v1_cmd: 'system', v1_action: 'force_update', v2: 'ota.update' },
  { v1_cmd: 'system', v1_action: 'custom_update', v2: 'ota.custom' },
]

// Transport badge styles
const TRANSPORT_META = {
  MQTT:      { color: 'var(--color-info)',    bg: 'var(--color-info-bg)' },
  WebSocket: { color: 'var(--color-primary)', bg: 'var(--color-primary-subtle)' },
  HTTP:      { color: 'var(--color-success)', bg: 'var(--color-success-bg)' },
  UART:      { color: 'var(--color-warning)', bg: 'var(--color-warning-bg)' },
  UDP:       { color: 'var(--color-info)',    bg: 'var(--color-info-bg)' },
  All:       { color: 'var(--color-text-muted)', bg: 'var(--color-bg-island)' },
}

// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------

function TransportPill({ name }) {
  const meta = TRANSPORT_META[name] || TRANSPORT_META.All
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center',
      padding: 'var(--space-1) var(--space-2)',
      borderRadius: 'var(--radius-full)',
      fontSize: 'var(--font-size-xs)',
      fontFamily: 'var(--font-family-mono)',
      fontWeight: 'var(--font-weight-medium)',
      color: meta.color,
      backgroundColor: meta.bg,
      whiteSpace: 'nowrap',
    }}>
      {name}
    </span>
  )
}

function CodeBlock({ code }) {
  const [copied, setCopied] = useState(false)
  const handleCopy = () => {
    navigator.clipboard.writeText(code).then(() => {
      setCopied(true)
      setTimeout(() => setCopied(false), 1800)
    })
  }
  return (
    <div style={{ position: 'relative' }} className="api-code-wrap">
      <pre style={{
        margin: 0, padding: 'var(--space-3)',
        borderRadius: 'var(--radius-md)',
        backgroundColor: 'var(--color-bg-abyss)',
        border: '1px solid var(--color-border)',
        fontFamily: 'var(--font-family-mono)',
        fontSize: 'var(--font-size-sm)',
        color: 'var(--color-success)',
        overflowX: 'auto', lineHeight: '1.6',
        whiteSpace: 'pre-wrap', wordBreak: 'break-word',
      }}>
        {code}
      </pre>
      <button
        type="button"
        onClick={handleCopy}
        className="api-copy-btn"
        aria-label="Copy code"
        style={{
          position: 'absolute', top: 'var(--space-2)', right: 'var(--space-2)',
          padding: '2px var(--space-2)',
          borderRadius: 'var(--radius-sm)',
          fontSize: 'var(--font-size-xs)',
          cursor: 'pointer',
          border: '1px solid var(--color-border-strong)',
          backgroundColor: copied ? 'var(--color-success-bg)' : 'var(--color-bg-elevated)',
          color: copied ? 'var(--color-success)' : 'var(--color-text-muted)',
          transition: 'all 0.15s ease',
          opacity: 0,
          fontFamily: 'var(--font-family-base)',
        }}
      >
        {copied ? 'Copied' : 'Copy'}
      </button>
    </div>
  )
}

function ReturnBlock({ code }) {
  return (
    <pre style={{
      margin: 0, padding: 'var(--space-3)',
      borderRadius: 'var(--radius-md)',
      backgroundColor: 'var(--color-bg-abyss)',
      border: '1px solid var(--color-border)',
      fontFamily: 'var(--font-family-mono)',
      fontSize: 'var(--font-size-sm)',
      color: 'var(--color-info)',
      overflowX: 'auto', lineHeight: '1.6',
      whiteSpace: 'pre-wrap', wordBreak: 'break-word',
    }}>
      {code}
    </pre>
  )
}

function CommandCard({ command }) {
  const [open, setOpen] = useState(false)
  const isUartAllowed = UART_WHITELIST.includes(command.cmd)

  return (
    <div
      className={`api-cmd-card${open ? ' api-cmd-card--open' : ''}`}
      style={{
        borderRadius: 'var(--radius-lg)',
        border: '1px solid var(--color-border)',
        overflow: 'hidden',
        backgroundColor: 'var(--color-bg-surface)',
        transition: 'border-color 0.15s ease',
      }}
    >
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        style={{
          width: '100%', display: 'flex', alignItems: 'center',
          justifyContent: 'space-between',
          padding: 'var(--space-3) var(--space-4)',
          background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left',
          gap: 'var(--space-3)',
        }}
        aria-expanded={open}
      >
        {/* Left */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)', minWidth: 0, flex: 1 }}>
          <span style={{
            fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-base)',
            fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-primary)', flexShrink: 0,
          }}>
            {command.cmd}
          </span>
          {command.warning && (
            <span style={{
              display: 'inline-flex', alignItems: 'center', gap: '4px',
              padding: '2px var(--space-2)', borderRadius: 'var(--radius-full)',
              fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-medium)',
              color: 'var(--color-warning)', backgroundColor: 'var(--color-warning-bg)', flexShrink: 0,
            }}>
              <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true"><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /></svg>
              caution
            </span>
          )}
          <span style={{
            fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)',
            overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
          }}>
            {command.description}
          </span>
        </div>
        {/* Right */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', flexShrink: 0 }}>
          {command.transports.map(t => <TransportPill key={t} name={t} />)}
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
            style={{ color: 'var(--color-text-muted)', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s ease', flexShrink: 0, marginLeft: 'var(--space-1)' }}
            aria-hidden="true"
          >
            <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
          </svg>
        </div>
      </button>

      {open && (
        <div style={{
          borderTop: '1px solid var(--color-border)',
          padding: 'var(--space-4)',
          display: 'flex', flexDirection: 'column', gap: 'var(--space-4)',
          backgroundColor: 'var(--color-bg-elevated)',
        }}>
          <p style={{ margin: 0, fontSize: 'var(--font-size-base)', color: 'var(--color-text-secondary)', lineHeight: '1.6' }}>
            {command.description}
          </p>

          {command.warning && (
            <div style={{
              display: 'flex', alignItems: 'flex-start', gap: 'var(--space-2)',
              padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
              backgroundColor: 'var(--color-warning-bg)', border: '1px solid var(--color-warning)',
              fontSize: 'var(--font-size-sm)', color: 'var(--color-warning)',
            }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ flexShrink: 0, marginTop: '1px' }} aria-hidden="true"><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /></svg>
              <span>{command.warning}</span>
            </div>
          )}

          <div className="api-detail-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
            {/* Left: meta + contents + returns */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
              {/* Meta */}
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-4)' }}>
                <div>
                  <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', marginBottom: 'var(--space-1)' }}>Handler</div>
                  <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-info)' }}>{command.handler}</span>
                </div>
                {isUartAllowed && (
                  <div>
                    <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', marginBottom: 'var(--space-1)' }}>UART</div>
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', padding: '2px var(--space-2)', borderRadius: 'var(--radius-full)', fontSize: 'var(--font-size-xs)', color: 'var(--color-success)', backgroundColor: 'var(--color-success-bg)' }}>
                      <svg width="6" height="6" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><circle cx="6" cy="6" r="6" /></svg>
                      whitelisted
                    </span>
                  </div>
                )}
              </div>

              {/* Contents */}
              {command.contents ? (
                <div>
                  <div style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', marginBottom: 'var(--space-2)' }}>
                    Contents
                  </div>
                  <div style={{ borderRadius: 'var(--radius-md)', overflow: 'hidden', border: '1px solid var(--color-border)' }}>
                    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                      <thead>
                        <tr style={{ backgroundColor: 'var(--color-bg-abyss)' }}>
                          {['Field', 'Type', 'Req', 'Notes'].map(h => (
                            <th key={h} style={{ padding: 'var(--space-2) var(--space-3)', textAlign: 'left', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', borderBottom: '1px solid var(--color-border)' }}>{h}</th>
                          ))}
                        </tr>
                      </thead>
                      <tbody>
                        {command.contents.map((field, i) => (
                          <tr key={field.field} style={{ borderTop: i > 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(192,193,255,0.015)' : 'transparent' }}>
                            <td style={{ padding: 'var(--space-2) var(--space-3)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-primary)' }}>{field.field}</td>
                            <td style={{ padding: 'var(--space-2) var(--space-3)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-info)' }}>{field.type}</td>
                            <td style={{ padding: 'var(--space-2) var(--space-3)', fontSize: 'var(--font-size-xs)' }}>
                              {field.required ? <span style={{ color: 'var(--color-danger)', fontWeight: 'var(--font-weight-semibold)' }}>yes</span> : <span style={{ color: 'var(--color-text-muted)' }}>no</span>}
                            </td>
                            <td style={{ padding: 'var(--space-2) var(--space-3)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{field.notes}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                </div>
              ) : (
                <p style={{ margin: 0, fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', fontStyle: 'italic' }}>No contents required.</p>
              )}

              {/* Returns */}
              {command.returns && (
                <div>
                  <div style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', marginBottom: 'var(--space-2)' }}>Returns</div>
                  <ReturnBlock code={command.returns} />
                </div>
              )}
            </div>

            {/* Right: example */}
            <div>
              <div style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', marginBottom: 'var(--space-2)' }}>Example</div>
              <CodeBlock code={command.example} />
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

function NamespaceSection({ ns, searchQuery }) {
  const filteredCommands = useMemo(() => {
    if (!searchQuery) return ns.commands
    const q = searchQuery.toLowerCase()
    return ns.commands.filter(c =>
      c.cmd.toLowerCase().includes(q) ||
      c.description.toLowerCase().includes(q) ||
      c.handler.toLowerCase().includes(q)
    )
  }, [ns.commands, searchQuery])

  if (searchQuery && filteredCommands.length === 0) return null

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--space-3)', paddingBottom: 'var(--space-3)', borderBottom: '1px solid var(--color-border)' }}>
        <h2 style={{ margin: 0, fontFamily: 'var(--font-family-display)', fontSize: 'var(--font-size-lg)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-primary)', letterSpacing: 'var(--tracking-tight)' }}>
          {ns.label === 'Root' ? 'root' : ns.label}
        </h2>
        <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: '20px', height: '20px', padding: '0 6px', borderRadius: 'var(--radius-full)', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-primary)', backgroundColor: 'var(--color-primary-subtle)' }}>
          {filteredCommands.length}
        </span>
        <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>{ns.description}</span>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
        {filteredCommands.map(cmd => <CommandCard key={cmd.cmd} command={cmd} />)}
      </div>
    </div>
  )
}

// ---------------------------------------------------------------------------
// Tab: Transports
// ---------------------------------------------------------------------------
function TransportsTab() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)', paddingTop: 'var(--space-2)' }}>
      <Card title="Transport Channels" subtitle="All available communication interfaces">
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse' }}>
            <thead>
              <tr style={{ backgroundColor: 'var(--color-bg-abyss)' }}>
                {['Transport', 'Address / Topic', 'Direction', 'Notes'].map(h => (
                  <th key={h} style={{ padding: 'var(--space-3) var(--space-4)', textAlign: 'left', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', borderBottom: '1px solid var(--color-border)', whiteSpace: 'nowrap' }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {TRANSPORTS.map((row, i) => (
                <tr key={i} style={{ borderTop: i > 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(192,193,255,0.015)' : 'transparent' }}>
                  <td style={{ padding: 'var(--space-3) var(--space-4)' }}><TransportPill name={row.transport} /></td>
                  <td style={{ padding: 'var(--space-3) var(--space-4)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-primary)' }}>{row.address}</td>
                  <td style={{ padding: 'var(--space-3) var(--space-4)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', whiteSpace: 'nowrap' }}>{row.direction}</td>
                  <td style={{ padding: 'var(--space-3) var(--space-4)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>{row.notes}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </Card>

      <Card title="UART Whitelist" subtitle="Commands permitted on the hardware serial interface">
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-2)' }}>
          {UART_WHITELIST.map(cmd => (
            <span key={cmd} style={{ display: 'inline-flex', alignItems: 'center', padding: 'var(--space-1) var(--space-3)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-primary)', backgroundColor: 'var(--color-primary-subtle)', border: '1px solid var(--color-border)' }}>
              {cmd}
            </span>
          ))}
        </div>
      </Card>

      <Card title="Message Format" subtitle="Standard envelope for all v2 commands">
        <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
          <CodeBlock code={'{\n  "v": 2,\n  "cmd": "<namespace>.<command>",\n  "contents": { ... }   // optional\n}'} />
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 'var(--space-3)' }}>
            {[
              { field: 'v', type: 'int', notes: 'Protocol version. Always 2 for v2 firmware.' },
              { field: 'cmd', type: 'string', notes: 'Command name, e.g. "relay.set_durations"' },
              { field: 'contents', type: 'object?', notes: 'Optional payload. Omit entirely if not needed.' },
            ].map(row => (
              <div key={row.field} style={{ padding: 'var(--space-3)', borderRadius: 'var(--radius-md)', backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', marginBottom: 'var(--space-1)' }}>
                  <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-primary)' }}>{row.field}</span>
                  <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-info)' }}>{row.type}</span>
                </div>
                <p style={{ margin: 0, fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>{row.notes}</p>
              </div>
            ))}
          </div>
        </div>
      </Card>
    </div>
  )
}

// ---------------------------------------------------------------------------
// Tab: Legacy migration
// ---------------------------------------------------------------------------
function LegacyTab() {
  return (
    <div style={{ paddingTop: 'var(--space-2)' }}>
    <Card title="v1 → v2 Command Migration" subtitle="Map from v1 cmd+action pairs to their v2 equivalents">
      <div style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr style={{ backgroundColor: 'var(--color-bg-abyss)' }}>
              {['v1 cmd', 'v1 action', 'v2 command'].map(h => (
                <th key={h} style={{ padding: 'var(--space-3) var(--space-4)', textAlign: 'left', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', borderBottom: '1px solid var(--color-border)' }}>{h}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {LEGACY_MAP.map((row, i) => (
              <tr key={i} style={{ borderTop: i > 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(192,193,255,0.015)' : 'transparent' }}>
                <td style={{ padding: 'var(--space-3) var(--space-4)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-warning)' }}>{row.v1_cmd}</td>
                <td style={{ padding: 'var(--space-3) var(--space-4)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{row.v1_action}</td>
                <td style={{ padding: 'var(--space-3) var(--space-4)', fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-primary)' }}>{row.v2}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </Card>
    </div>
  )
}

// ---------------------------------------------------------------------------
// Tab: Commands
// ---------------------------------------------------------------------------
function CommandsTab({ activeNamespace, setActiveNamespace, searchQuery, setSearchQuery }) {
  const totalCmds = NAMESPACES.reduce((acc, ns) => acc + ns.commands.length, 0)
  const nsTabs = [{ key: 'all', label: 'All', count: totalCmds }, ...NAMESPACES.map(ns => ({ key: ns.id, label: ns.label, count: ns.commands.length }))]

  const visibleNamespaces = useMemo(() => {
    if (activeNamespace === 'all') return NAMESPACES
    return NAMESPACES.filter(ns => ns.id === activeNamespace)
  }, [activeNamespace])

  const noResults = searchQuery && visibleNamespaces.every(ns => {
    const q = searchQuery.toLowerCase()
    return ns.commands.filter(c => c.cmd.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) || c.handler.toLowerCase().includes(q)).length === 0
  })

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)', paddingTop: 'var(--space-2)' }}>
      {/* Filters row */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', flexWrap: 'wrap' }}>
        <div style={{ flex: '1 1 200px', maxWidth: '340px' }}>
          <SearchBar value={searchQuery} onChange={setSearchQuery} placeholder="Search commands…" />
        </div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-2)' }}>
          {nsTabs.map(tab => (
            <button
              key={tab.key}
              type="button"
              onClick={() => setActiveNamespace(tab.key)}
              style={{
                display: 'inline-flex', alignItems: 'center', gap: 'var(--space-1)',
                padding: 'var(--space-1) var(--space-3)',
                borderRadius: 'var(--radius-md)',
                fontSize: 'var(--font-size-sm)',
                fontFamily: 'var(--font-family-mono)',
                fontWeight: activeNamespace === tab.key ? 'var(--font-weight-semibold)' : 'var(--font-weight-normal)',
                cursor: 'pointer',
                border: '1px solid',
                borderColor: activeNamespace === tab.key ? 'var(--color-primary)' : 'var(--color-border-strong)',
                backgroundColor: activeNamespace === tab.key ? 'var(--color-primary-subtle)' : 'transparent',
                color: activeNamespace === tab.key ? 'var(--color-primary)' : 'var(--color-text-muted)',
                transition: 'all 0.15s ease',
              }}
            >
              {tab.label}
              <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: '16px', height: '16px', padding: '0 4px', borderRadius: 'var(--radius-full)', fontSize: 'var(--font-size-xs)', backgroundColor: activeNamespace === tab.key ? 'var(--color-primary-subtle)' : 'var(--color-bg-island)', color: activeNamespace === tab.key ? 'var(--color-primary)' : 'var(--color-text-muted)' }}>
                {tab.count}
              </span>
            </button>
          ))}
        </div>
      </div>

      {/* Command list */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)' }}>
        {visibleNamespaces.map(ns => <NamespaceSection key={ns.id} ns={ns} searchQuery={searchQuery} />)}
        {noResults && (
          <div style={{ textAlign: 'center', padding: 'var(--space-12)', color: 'var(--color-text-muted)', fontSize: 'var(--font-size-base)' }}>
            No commands match "{searchQuery}"
          </div>
        )}
      </div>
    </div>
  )
}

// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default function ApiReferencePage() {
  const [activeTab, setActiveTab] = useState('commands')
  const [activeNamespace, setActiveNamespace] = useState('all')
  const [searchQuery, setSearchQuery] = useState('')

  const TABS = [
    { key: 'commands', label: 'Commands' },
    { key: 'transports', label: 'Transports' },
    { key: 'legacy', label: 'v1 → v2 Migration' },
  ]

  const totalCmds = NAMESPACES.reduce((a, n) => a + n.commands.length, 0)

  return (
    <>
      <style>{`
        .api-code-wrap:hover .api-copy-btn { opacity: 1 !important; }
        .api-cmd-card:hover { border-color: var(--color-border-strong) !important; }
        .api-cmd-card--open { border-color: rgba(192,193,255,0.25) !important; }
        @media (max-width: 768px) {
          .api-detail-grid { grid-template-columns: 1fr !important; }
        }
      `}</style>

      <div className="page-wrapper">
        <PageHeader
          title="API Reference"
          subtitle="Vesper Firmware Command Protocol v2"
        />

        {/* Stats strip */}
        <div style={{ display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
          {[
            { label: 'Namespaces', value: NAMESPACES.length },
            { label: 'Commands', value: totalCmds },
            { label: 'Transports', value: 5 },
            { label: 'Protocol', value: 'v2' },
          ].map(stat => (
            <div key={stat.label} style={{
              display: 'flex', alignItems: 'center', gap: 'var(--space-3)',
              padding: 'var(--space-3) var(--space-4)',
              borderRadius: 'var(--radius-lg)',
              backgroundColor: 'var(--color-bg-surface)',
              border: '1px solid var(--color-border)',
              boxShadow: 'var(--shadow-card)',
            }}>
              <span style={{ fontFamily: 'var(--font-family-display)', fontSize: 'var(--font-size-xl)', fontWeight: 'var(--font-weight-bold)', color: 'var(--color-primary)', lineHeight: 1 }}>
                {stat.value}
              </span>
              <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>
                {stat.label}
              </span>
            </div>
          ))}
        </div>

        <Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} variant="line" />

        {activeTab === 'commands' && (
          <CommandsTab
            activeNamespace={activeNamespace}
            setActiveNamespace={setActiveNamespace}
            searchQuery={searchQuery}
            setSearchQuery={setSearchQuery}
          />
        )}
        {activeTab === 'transports' && <TransportsTab />}
        {activeTab === 'legacy' && <LegacyTab />}
      </div>
    </>
  )
}
