// Compact voice UI for narrow / mobile layouts (Plan E hybrid:
// AppBar mutes + status chip with 2-line live readout + wide bottom-
// anchored PTT button + modal sheet for non-essential controls).
//
// rc.8 follow-up: the AppBar gear icon was removed; the modal sheet
// is now the **single** voice-controls surface on mobile. Mode +
// release-tail are surfaced inline (radio buttons + slider) inside
// the modal.

import 'dart:async' show StreamSubscription, Timer, unawaited;
import 'dart:io' show Platform;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:haptic_kit/haptic_kit.dart' show Haptics;

import '../l10n/generated/app_localizations.dart';
import 'audio_output_tile.dart';
import 'audio_processing_config_state.dart';
import 'ptt_capability_badge.dart';
import 'talk_power_warning.dart';
import 'voice_haptics.dart';
import 'voice_level_meter.dart';
import 'voice_settings_controls.dart';
import 'voice_status_summary.dart';
import '../src/rust/api.dart' as rust;

bool get _isIos {
  if (kIsWeb) return false;
  return Platform.isIOS;
}

bool get _isMacOS {
  if (kIsWeb) return false;
  return Platform.isMacOS;
}

bool get _isDesktopSileroVadHost {
  if (kIsWeb) return false;
  return Platform.isWindows || Platform.isLinux;
}

/// Two-line status chip that summarises the current voice state.
/// Tap to open the voice details modal.
class VoiceStatusChip extends StatelessWidget {
  /// Construct a status chip.
  const VoiceStatusChip({
    super.key,
    required this.transmitMode,
    required this.releaseTailMs,
    required this.pttBoundKeyLabel,
    required this.audioStats,
    required this.isTouchOnly,
    required this.onTap,
    required this.onToggleInputMute,
    required this.onToggleOutputMute,
    this.inputMuted = false,
    this.outputMuted = false,
    this.hardMuteByTalkPower = false,
    this.talkPower,
    this.neededTalkPower,
    this.talkPowerGranted,
  });

  /// Current transmit mode.
  final rust.BridgeTransmitMode transmitMode;

  /// Release-tail in milliseconds.
  final int releaseTailMs;

  /// Bound key label (empty on touch-only hosts).
  final String pttBoundKeyLabel;

  /// Current audio stats; null while audio engine not running.
  final rust.BridgeAudioStats? audioStats;

  /// True on iOS / iPadOS / Android.
  final bool isTouchOnly;

  /// True when local mic is muted (hard mute or permission mute).
  final bool inputMuted;

  /// True when local speaker is muted.
  final bool outputMuted;

  /// True when the server talk-power gate forces local hard mute.
  final bool hardMuteByTalkPower;

  /// Own client's talk power.
  final int? talkPower;

  /// Talk power required to speak in current channel.
  final int? neededTalkPower;

  /// True when server granted talk power regardless of numeric value.
  final bool? talkPowerGranted;

  /// Open the voice details modal.
  final VoidCallback onTap;

  /// Toggle local input hard mute.
  final VoidCallback onToggleInputMute;

  /// Toggle local output mute/deafen.
  final VoidCallback onToggleOutputMute;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppL10n.of(context);

    final summary = voiceStatusSummary(
      l10n: l10n,
      transmitMode: transmitMode,
      releaseTailMs: releaseTailMs,
      pttBoundKeyLabel: pttBoundKeyLabel,
      isTouchOnly: isTouchOnly,
      inputMuted: inputMuted,
      outputMuted: outputMuted,
      pttActive: audioStats?.pttActive ?? false,
      talkPower: talkPower,
      neededTalkPower: neededTalkPower,
      talkPowerGranted: talkPowerGranted,
    );

    return Semantics(
      label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
      child: Material(
        type: MaterialType.transparency,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          decoration: BoxDecoration(
            color: summary.talkPowerBlocked
                ? Colors.amber.withValues(alpha: 0.18)
                : summary.muted
                ? theme.colorScheme.errorContainer.withValues(alpha: 0.35)
                : theme.colorScheme.surfaceContainerHigh,
            borderRadius: BorderRadius.circular(12),
            border: Border.all(
              color: summary.talkPowerBlocked
                  ? Colors.amber.shade700
                  : summary.muted
                  ? theme.colorScheme.error
                  : theme.colorScheme.outlineVariant,
              width: summary.talkPowerBlocked || summary.muted ? 1.5 : 0.5,
            ),
          ),
          child: Row(
            children: [
              Icon(
                summary.micOn
                    ? Icons.fiber_manual_record
                    : Icons.fiber_manual_record_outlined,
                size: 12,
                color: summary.micOn
                    ? theme.colorScheme.primary
                    : theme.colorScheme.outline,
              ),
              const SizedBox(width: 8),
              Expanded(
                child: InkWell(
                  onTap: () {
                    HapticFeedback.lightImpact();
                    onTap();
                  },
                  borderRadius: BorderRadius.circular(8),
                  child: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 2),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text(
                          summary.line1,
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                          style: theme.textTheme.bodyMedium?.copyWith(
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                        Text(
                          summary.line2,
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                          style: theme.textTheme.bodySmall?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              const SizedBox(width: 4),
              IconButton(
                tooltip: hardMuteByTalkPower
                    ? l10n.voiceTalkPowerBlocked
                    : l10n.voiceHardMuteLabel,
                icon: Icon(inputMuted ? Icons.mic_off : Icons.mic),
                color: inputMuted ? theme.colorScheme.error : null,
                onPressed: hardMuteByTalkPower ? null : onToggleInputMute,
                visualDensity: VisualDensity.compact,
                constraints: const BoxConstraints.tightFor(
                  width: 40,
                  height: 40,
                ),
                padding: EdgeInsets.zero,
                style: const ButtonStyle(
                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                ),
              ),
              IconButton(
                tooltip: l10n.voiceOutputMuteLabel,
                icon: Icon(outputMuted ? Icons.headset_off : Icons.headset),
                color: outputMuted ? theme.colorScheme.error : null,
                onPressed: onToggleOutputMute,
                visualDensity: VisualDensity.compact,
                constraints: const BoxConstraints.tightFor(
                  width: 40,
                  height: 40,
                ),
                padding: EdgeInsets.zero,
                style: const ButtonStyle(
                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                ),
              ),
              IconButton(
                tooltip: l10n.voiceSettingsTitle,
                icon: Icon(
                  Icons.expand_less,
                  size: 18,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
                onPressed: onTap,
                visualDensity: VisualDensity.compact,
                constraints: const BoxConstraints.tightFor(
                  width: 40,
                  height: 40,
                ),
                padding: EdgeInsets.zero,
                style: const ButtonStyle(
                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// Wide, bottom-anchored push-to-talk button.
class VoicePttButton extends StatefulWidget {
  /// Construct a PTT button.
  const VoicePttButton({
    super.key,
    required this.active,
    required this.onHeldChanged,
    this.height = 56,
    this.borderRadius = 16,
    this.iconSize = 28,
    this.iconGap = 12,
    this.blurRadius = 16,
    this.spreadRadius = 2,
    this.listenForPan = false,
    this.labelLetterSpacing = 0.4,
  });

  /// True while the engine reports the gate open.
  final bool active;

  /// Called with `true` on finger-down, `false` on finger-up or cancel.
  final ValueChanged<bool> onHeldChanged;

  /// Button height.
  final double height;

  /// Outer corner radius.
  final double borderRadius;

  /// Mic icon size.
  final double iconSize;

  /// Space between icon and label.
  final double iconGap;

  /// Active-state shadow blur radius.
  final double blurRadius;

  /// Active-state shadow spread radius.
  final double spreadRadius;

  /// Also react to pan start/end/cancel in addition to tap gestures.
  final bool listenForPan;

  /// Optional label letter spacing.
  final double? labelLetterSpacing;

  @override
  State<VoicePttButton> createState() => _VoicePttButtonState();
}

class _VoicePttButtonState extends State<VoicePttButton> {
  bool _pressed = false;

  @override
  void initState() {
    super.initState();
    prepareVoiceHaptics();
  }

  void _setHeld(bool held) {
    if (_pressed == held) return;
    setState(() => _pressed = held);
    widget.onHeldChanged(held);
    playVoicePttHaptic(held);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppL10n.of(context);
    final activeNow = _pressed || widget.active;

    return Semantics(
      button: true,
      liveRegion: true,
      label: activeNow ? l10n.pttTransmitting : l10n.pttHoldToTalk,
      hint: l10n.pttHoldToTalkSemanticsHint,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTapDown: (_) => _setHeld(true),
        onTapUp: (_) => _setHeld(false),
        onTapCancel: () => _setHeld(false),
        onPanDown: widget.listenForPan ? (_) => _setHeld(true) : null,
        onPanEnd: widget.listenForPan ? (_) => _setHeld(false) : null,
        onPanCancel: widget.listenForPan ? () => _setHeld(false) : null,
        child: ExcludeSemantics(
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 80),
            height: widget.height,
            decoration: BoxDecoration(
              color: activeNow
                  ? theme.colorScheme.primary
                  : theme.colorScheme.primaryContainer,
              borderRadius: BorderRadius.circular(widget.borderRadius),
              boxShadow: activeNow
                  ? [
                      BoxShadow(
                        color: theme.colorScheme.primary.withAlpha(100),
                        blurRadius: widget.blurRadius,
                        spreadRadius: widget.spreadRadius,
                        offset: const Offset(0, 2),
                      ),
                    ]
                  : null,
            ),
            child: Center(
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    activeNow ? Icons.mic : Icons.mic_none,
                    color: activeNow
                        ? theme.colorScheme.onPrimary
                        : theme.colorScheme.onPrimaryContainer,
                    size: widget.iconSize,
                  ),
                  SizedBox(width: widget.iconGap),
                  Text(
                    activeNow ? l10n.voiceMicOn : l10n.voiceModePtt,
                    style: theme.textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.w600,
                      letterSpacing: widget.labelLetterSpacing,
                      color: activeNow
                          ? theme.colorScheme.onPrimary
                          : theme.colorScheme.onPrimaryContainer,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// Show the voice controls modal sheet — the single voice-controls
/// surface on mobile. Tiles:
///   1. Audio output route picker (iOS native AVRoutePickerView /
///      Android Material 3 list). Mobile only.
///   2. Mode radio buttons (PTT / Continuous / Voice Activity).
///   3. Release-tail slider (PTT-only).
///   4. Mic level meter + frame counts.
///   5. Audio processing (NS · AEC · AGC · HPF · VAD) — all modes.
///   6. PTT capability badge.
///
/// Mode + release-tail are inlined directly here instead of being
/// hidden behind an "Adjust" button → nested dialog. Single-screen
/// control panel, zero navigation depth. `onModeChanged` and
/// `onReleaseTailChanged` are debounced by the caller so users can
/// drag the slider freely.
Future<void> showVoiceDetailsSheet(
  BuildContext context, {
  required rust.BridgeTransmitMode transmitMode,
  required int releaseTailMs,
  required String pttBoundKeyLabel,
  required String pttLevel,
  required String pttBackendId,
  required String pttBoundInputClass,
  required bool isTouchOnly,
  required rust.BridgeAudioProcessingConfig initialAudioConfig,
  required ValueChanged<rust.BridgeTransmitMode> onModeChanged,
  required ValueChanged<int> onReleaseTailChanged,
  required ValueChanged<rust.BridgeAudioProcessingConfig> onAudioConfigChanged,
  int? talkPower,
  int? neededTalkPower,
  bool? talkPowerGranted,
}) async {
  await showModalBottomSheet<void>(
    context: context,
    showDragHandle: true,
    isScrollControlled: true,
    useSafeArea: true,
    builder: (ctx) {
      return DraggableScrollableSheet(
        initialChildSize: 0.6,
        minChildSize: 0.3,
        maxChildSize: 0.95,
        expand: false,
        snap: true,
        snapSizes: const [0.3, 0.6, 0.95],
        builder: (ctx, scrollController) => _VoiceSheetBody(
          scrollController: scrollController,
          initialMode: transmitMode,
          initialReleaseTailMs: releaseTailMs,
          pttBoundKeyLabel: pttBoundKeyLabel,
          pttLevel: pttLevel,
          pttBackendId: pttBackendId,
          pttBoundInputClass: pttBoundInputClass,
          isTouchOnly: isTouchOnly,
          initialAudioConfig: initialAudioConfig,
          onModeChanged: onModeChanged,
          onReleaseTailChanged: onReleaseTailChanged,
          onAudioConfigChanged: onAudioConfigChanged,
          talkPower: talkPower,
          neededTalkPower: neededTalkPower,
          talkPowerGranted: talkPowerGranted,
        ),
      );
    },
  );
}

class _VoiceSheetBody extends StatefulWidget {
  const _VoiceSheetBody({
    required this.scrollController,
    required this.initialMode,
    required this.initialReleaseTailMs,
    required this.pttBoundKeyLabel,
    required this.pttLevel,
    required this.pttBackendId,
    required this.pttBoundInputClass,
    required this.isTouchOnly,
    required this.initialAudioConfig,
    required this.onModeChanged,
    required this.onReleaseTailChanged,
    required this.onAudioConfigChanged,
    this.talkPower,
    this.neededTalkPower,
    this.talkPowerGranted,
  });

  final ScrollController scrollController;
  final rust.BridgeTransmitMode initialMode;
  final int initialReleaseTailMs;
  final String pttBoundKeyLabel;
  final String pttLevel;
  final String pttBackendId;
  final String pttBoundInputClass;
  final bool isTouchOnly;
  final rust.BridgeAudioProcessingConfig initialAudioConfig;
  final ValueChanged<rust.BridgeTransmitMode> onModeChanged;
  final ValueChanged<int> onReleaseTailChanged;
  final ValueChanged<rust.BridgeAudioProcessingConfig> onAudioConfigChanged;
  final int? talkPower;
  final int? neededTalkPower;
  final bool? talkPowerGranted;

  @override
  State<_VoiceSheetBody> createState() => _VoiceSheetBodyState();
}

class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
  late rust.BridgeTransmitMode _mode = widget.initialMode;
  late int _tail = widget.initialReleaseTailMs;

  // Live stats — polled by this widget's own timer so TX/RX update
  // in real time while the sheet is open, independent of the parent.
  rust.BridgeAudioStats? _stats;
  Timer? _statsTimer;
  // Previous snapshot for computing per-second rates.
  int _prevSent = 0;
  int _prevReceived = 0;
  int _txRate = 0; // frames/s
  int _rxRate = 0; // frames/s
  int _rateTickCount = 0;

  late final AudioProcessingConfigState _audioProcessing;
  double? _streamLevel;
  StreamSubscription<double>? _levelSub;

  @override
  void initState() {
    super.initState();
    _audioProcessing = AudioProcessingConfigState.fromConfig(
      widget.initialAudioConfig,
    );

    _levelSub = rust.inputLevelStream().listen((level) {
      if (mounted) setState(() => _streamLevel = level);
    });

    // Poll audio stats at 250 ms so TX/RX counters update in real time
    // while the sheet is open.
    _statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async {
      try {
        final s = await rust.audioStats();
        if (!mounted) return;
        setState(() {
          _stats = s;
          _rateTickCount++;
          if (_rateTickCount >= 4) {
            _txRate = s.framesSent - _prevSent;
            _rxRate = s.framesReceived - _prevReceived;
            _prevSent = s.framesSent;
            _prevReceived = s.framesReceived;
            _rateTickCount = 0;
          }
        });
      } catch (_) {}
    });
  }

  @override
  void dispose() {
    _levelSub?.cancel();
    _statsTimer?.cancel();
    super.dispose();
  }

  rust.BridgeAudioProcessingConfig _buildConfig() {
    return _audioProcessing.buildConfig(base: widget.initialAudioConfig);
  }

  void _notifyAudioConfig() {
    widget.onAudioConfigChanged(_buildConfig());
  }

  void _setMode(rust.BridgeTransmitMode m) {
    if (m == _mode) return;
    setState(() => _mode = m);
    unawaited(Haptics.selection().catchError((_) {}));
    widget.onModeChanged(m);
  }

  void _setTail(double v) {
    final ms = v.round();
    setState(() => _tail = ms);
    widget.onReleaseTailChanged(ms);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppL10n.of(context);
    final stats = _stats;
    // Level meter active = transmitting (any mode).
    final levelActive = switch (_mode) {
      rust.BridgeTransmitMode.continuous => true,
      _ => stats?.pttActive ?? false,
    };
    final isPtt = _mode == rust.BridgeTransmitMode.ptt;

    // Route picker is mobile-only. iOS uses AVAudioSession below;
    // Android uses AudioManager through a MethodChannel.
    final showRoutePicker = !kIsWeb && (Platform.isIOS || Platform.isAndroid);

    return SafeArea(
      child: SingleChildScrollView(
        controller: widget.scrollController,
        padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge),
            const SizedBox(height: 12),

            // 1) Audio output route picker tile (mobile only).
            if (showRoutePicker) ...[
              const AudioOutputTile(),
              const SizedBox(height: 8),
              Divider(height: 1, color: theme.colorScheme.outlineVariant),
              const SizedBox(height: 8),
            ],

            // Transmit mode.
            Text(
              l10n.voiceModeLabel,
              style: theme.textTheme.labelLarge?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 4),
            _ModeRow(
              label: l10n.voiceModePtt,
              icon: Icons.radio_button_checked,
              selected: _mode == rust.BridgeTransmitMode.ptt,
              onTap: () => _setMode(rust.BridgeTransmitMode.ptt),
            ),
            _ModeRow(
              label: l10n.voiceModeContinuous,
              icon: Icons.podcasts,
              selected: _mode == rust.BridgeTransmitMode.continuous,
              onTap: () => _setMode(rust.BridgeTransmitMode.continuous),
            ),
            _ModeRow(
              label: l10n.voiceModeVoiceActivity,
              icon: Icons.graphic_eq,
              selected: _mode == rust.BridgeTransmitMode.voiceActivity,
              onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity),
            ),

            // 3) Release-tail slider (PTT only).
            if (isPtt) ...[
              const SizedBox(height: 8),
              Row(
                children: [
                  Text(
                    l10n.voiceReleaseTailLabel,
                    style: theme.textTheme.labelLarge?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                  const Spacer(),
                  Text(
                    '$_tail${l10n.voiceReleaseTailHint}',
                    style: theme.textTheme.bodyMedium?.copyWith(
                      fontFeatures: const [FontFeature.tabularFigures()],
                    ),
                  ),
                ],
              ),
              Slider(
                value: _tail.toDouble().clamp(0, 500),
                min: 0,
                max: 500,
                divisions: 20,
                label: '$_tail ms',
                onChanged: _setTail,
              ),
              // Desktop-only: surface the bound key so the user
              // sees what hardware key is wired. On mobile this
              // row is suppressed (there is no hardware key; the
              // PTT button is the input).
              if (!widget.isTouchOnly &&
                  widget.pttBoundKeyLabel.isNotEmpty) ...[
                Padding(
                  padding: const EdgeInsets.only(left: 4),
                  child: Text(
                    '${l10n.voiceBoundKeyLabel}: ${widget.pttBoundKeyLabel}',
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                ),
              ],
            ],

            const SizedBox(height: 16),
            Divider(height: 1, color: theme.colorScheme.outlineVariant),
            const SizedBox(height: 12),

            // 4) Level meter + live TX/RX stats.
            VoiceLevelMeter(active: levelActive, level: _streamLevel ?? stats?.inputLevel),
            const SizedBox(height: 6),
            _StatsRow(
              txRate: _txRate,
              rxRate: _rxRate,
              totalSent: stats?.framesSent ?? 0,
              totalReceived: stats?.framesReceived ?? 0,
              transmitting: levelActive,
            ),

            if (isTalkPowerBlocked(
              talkPower: widget.talkPower,
              neededTalkPower: widget.neededTalkPower,
              talkPowerGranted: widget.talkPowerGranted,
            )) ...[
              const SizedBox(height: 8),
              TalkPowerWarning(
                talkPower: widget.talkPower,
                neededTalkPower: widget.neededTalkPower,
                talkPowerGranted: widget.talkPowerGranted,
              ),
            ],

            // Audio processing.
            const SizedBox(height: 12),
            Divider(height: 1, color: theme.colorScheme.outlineVariant),
            const SizedBox(height: 8),
            Text(
              'Audio processing',
              style: theme.textTheme.labelLarge?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 4),

            // Android HW/SW selector.
            if (Platform.isAndroid) ...[
              const VoiceSubHeader('Processing backend'),
              SegmentedButton<bool>(
                style: voiceSegmentedButtonStyle(theme),
                segments: androidProcessingSegments,
                selected: {_audioProcessing.preferHardware},
                onSelectionChanged: (s) {
                  setState(() => _audioProcessing.preferHardware = s.first);
                  _notifyAudioConfig();
                },
              ),
              const SizedBox(height: 4),
              Text(
                _audioProcessing.preferHardware
                    ? 'Hardware mode still keeps per-stage WebRTC fallback, so these controls remain effective.'
                    : 'Software mode applies the full WebRTC APM stage set.',
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ],

            if (_isIos) ...[
              Text(
                'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
                'hidden here; only settings that still affect the shipping '
                'iOS path are shown.',
                style: theme.textTheme.bodySmall?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
              const SizedBox(height: 4),
            ],
            if (!_isIos &&
                (!Platform.isAndroid ||
                    androidShowsNsControl(_audioProcessing)))
              AudioProcessingToggleRow(
                dense: true,
                label: 'Noise suppression',
                subtitle: 'Wiener filter',
                value: _audioProcessing.nsEnabled,
                onChanged: (v) {
                  setState(() => _audioProcessing.nsEnabled = v);
                  _notifyAudioConfig();
                },
              ),
            if (!_isIos &&
                (!Platform.isAndroid ||
                    androidShowsAecControl(_audioProcessing)))
              AudioProcessingToggleRow(
                dense: true,
                label: 'Echo cancellation',
                subtitle: Platform.isAndroid
                    ? (_audioProcessing.preferHardware
                          ? 'Prefers device/OS effect; falls back to WebRTC AEC3'
                          : 'WebRTC AEC3 · adaptive filter')
                    : (_isMacOS
                          ? 'Managed by platform VPIO'
                          : 'WebRTC AEC3 · adaptive filter'),
                value: _isMacOS ? true : _audioProcessing.aecEnabled,
                onChanged: _isMacOS
                    ? null
                    : (v) {
                        setState(() => _audioProcessing.aecEnabled = v);
                        _notifyAudioConfig();
                      },
              ),
            if (!_isIos &&
                (!Platform.isAndroid ||
                    androidShowsAgcControl(_audioProcessing)))
              AudioProcessingToggleRow(
                dense: true,
                label: 'Auto gain control',
                subtitle: 'AGC2 · -18 dBFS target',
                value: _audioProcessing.agcEnabled,
                onChanged: (v) {
                  setState(() => _audioProcessing.agcEnabled = v);
                  _notifyAudioConfig();
                },
              ),
            if (!Platform.isAndroid || androidShowsHpfControl(_audioProcessing))
              AudioProcessingToggleRow(
                dense: true,
                label: 'High-pass filter',
                subtitle: '80 Hz · DC removal',
                value: _audioProcessing.hpfEnabled,
                onChanged: (v) {
                  setState(() => _audioProcessing.hpfEnabled = v);
                  _notifyAudioConfig();
                },
              ),
            if (!_isIos &&
                (!Platform.isAndroid ||
                    androidShowsLimiterControl(_audioProcessing)))
              AudioProcessingToggleRow(
                dense: true,
                label: 'Peak limiter',
                subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
                value: _audioProcessing.limiterEnabled,
                onChanged: (v) {
                  setState(() => _audioProcessing.limiterEnabled = v);
                  _notifyAudioConfig();
                },
              ),

            // VAD backend.
            const SizedBox(height: 8),
            Text(
              'Voice activity detection (VAD)',
              style: theme.textTheme.labelLarge?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 2),
            SegmentedButton<rust.BridgeVadBackend>(
              style: voiceSegmentedButtonStyle(theme),
              segments: _isDesktopSileroVadHost
                  ? desktopVadBackendSegments
                  : vadBackendSegments,
              selected: {_audioProcessing.vadBackend},
              onSelectionChanged: (s) {
                setState(() => _audioProcessing.vadBackend = s.first);
                _notifyAudioConfig();
              },
            ),

            // Debug.
            const SizedBox(height: 8),
            Text(
              'Debug',
              style: theme.textTheme.labelLarge?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 2),
            AudioProcessingToggleRow(
              dense: true,
              label: 'WAV dump',
              subtitle: 'Record raw/processed mic to temp dir',
              value: _audioProcessing.debugWavDump,
              onChanged: (v) {
                setState(() => _audioProcessing.debugWavDump = v);
                _notifyAudioConfig();
              },
            ),

            // 6) PTT capability badge. On iOS this must remain
            // visible even though the resolved level is always
            // `L0Focused`, because the P0 acceptance flow requires
            // honest capability advertising with an explanation of
            // the sandbox limitation.
            if (isPtt) ...[
              const SizedBox(height: 12),
              PttCapabilityBadge(
                level: widget.pttLevel,
                backendId: widget.pttBackendId,
                boundInputClass: widget.pttBoundInputClass,
              ),
            ],
          ],
        ),
      ),
    );
  }
}

// ── Live TX/RX stats row ──────────────────────────────────────────────────

class _StatsRow extends StatelessWidget {
  const _StatsRow({
    required this.txRate,
    required this.rxRate,
    required this.totalSent,
    required this.totalReceived,
    required this.transmitting,
  });

  final int txRate;
  final int rxRate;
  final int totalSent;
  final int totalReceived;
  final bool transmitting;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final txColor = transmitting
        ? theme.colorScheme.primary
        : theme.colorScheme.onSurfaceVariant;
    return Row(
      children: [
        // TX
        Icon(Icons.upload, size: 12, color: txColor),
        const SizedBox(width: 3),
        Text(
          'TX $txRate/s · $totalSent',
          style: theme.textTheme.bodySmall?.copyWith(color: txColor),
        ),
        const SizedBox(width: 12),
        // RX
        Icon(
          Icons.download,
          size: 12,
          color: theme.colorScheme.onSurfaceVariant,
        ),
        const SizedBox(width: 3),
        Text(
          'RX $rxRate/s · $totalReceived',
          style: theme.textTheme.bodySmall?.copyWith(
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
      ],
    );
  }
}

// ── Audio processing helper widgets ──────────────────────────────────────

// ── Mode row ──────────────────────────────────────────────────────────────

class _ModeRow extends StatelessWidget {
  const _ModeRow({
    required this.label,
    required this.icon,
    required this.selected,
    required this.onTap,
  });

  final String label;
  final IconData icon;
  final bool selected;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final disabled = onTap == null;
    final color = disabled
        ? theme.colorScheme.onSurfaceVariant.withAlpha(120)
        : selected
        ? theme.colorScheme.primary
        : theme.colorScheme.onSurface;
    return Semantics(
      button: true,
      selected: selected,
      enabled: !disabled,
      label: label,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(8),
        child: ExcludeSemantics(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
            child: Row(
              children: [
                Icon(
                  selected
                      ? Icons.radio_button_checked
                      : Icons.radio_button_unchecked,
                  size: 20,
                  color: color,
                ),
                const SizedBox(width: 12),
                Icon(icon, size: 18, color: color),
                const SizedBox(width: 8),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        label,
                        style: theme.textTheme.bodyLarge?.copyWith(
                          color: color,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
