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

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

import '../l10n/generated/app_localizations.dart';
import '../services/android_audio_output_devices.dart';

/// Tile that displays the active audio output route and opens the
/// native picker on tap.
class AudioOutputTile extends StatefulWidget {
  /// Construct an audio output tile.
  const AudioOutputTile({super.key});

  @override
  State<AudioOutputTile> createState() => AudioOutputTileState();
}

/// State for [AudioOutputTile].
class AudioOutputTileState extends State<AudioOutputTile> {
  static const _androidOutputChannel = MethodChannel('app.audio_output');
  static const _androidOutputEvents = EventChannel('app.audio_output/events');

  AVAudioSessionPortDescription? _activeOutput;
  StreamSubscription<AVAudioSessionRouteChange>? _routeSub;
  StreamSubscription<dynamic>? _androidOutputSub;
  List<AndroidAudioOutputDevice> _androidDevices = const [];
  bool _androidLoading = false;

  @override
  void initState() {
    super.initState();
    if (!kIsWeb && Platform.isAndroid) {
      _refreshAndroidDevices();
      _androidOutputSub = _androidOutputEvents.receiveBroadcastStream().listen((
        _,
      ) {
        if (!mounted) return;
        _refreshAndroidDevices();
      });
      return;
    }
    _refresh();
    _routeSub = AVAudioSession().routeChangeStream.listen((_) {
      if (!mounted) return;
      _refresh();
    });
  }

  @override
  void dispose() {
    _routeSub?.cancel();
    _routeSub = null;
    _androidOutputSub?.cancel();
    _androidOutputSub = null;
    super.dispose();
  }

  Future<void> _refreshAndroidDevices() async {
    if (_androidLoading) return;
    setState(() => _androidLoading = true);
    try {
      final raw =
          await _androidOutputChannel.invokeListMethod<dynamic>(
            'getOutputDevices',
          ) ??
          const [];
      final devices = parseAndroidAudioOutputDevices(raw);
      if (!mounted) return;
      setState(() {
        _androidDevices = devices;
        _androidLoading = false;
      });
    } catch (_) {
      if (!mounted) return;
      setState(() => _androidLoading = false);
    }
  }

  Future<void> _refresh() async {
    try {
      final route = await AVAudioSession().currentRoute;
      if (!mounted) return;
      setState(() {
        _activeOutput = route.outputs.firstOrNull;
      });
    } catch (_) {
      // AVAudioSession may transiently throw before the session is active.
    }
  }

  static String portLabel(
    AVAudioSessionPort? type,
    String fallback,
    AppL10n l10n,
  ) {
    switch (type) {
      case AVAudioSessionPort.builtInSpeaker:
        return l10n.audioRouteSpeaker;
      case AVAudioSessionPort.builtInReceiver:
        return l10n.audioRouteReceiver;
      case AVAudioSessionPort.bluetoothHfp:
      case AVAudioSessionPort.bluetoothA2dp:
      case AVAudioSessionPort.bluetoothLe:
        return fallback.isEmpty ? l10n.audioRouteBluetooth : fallback;
      case AVAudioSessionPort.headphones:
      case AVAudioSessionPort.headsetMic:
        return fallback.isEmpty ? l10n.audioRouteWiredHeadset : fallback;
      case AVAudioSessionPort.carAudio:
        return l10n.audioRouteCarAudio;
      case AVAudioSessionPort.airPlay:
        return l10n.audioRouteAirplay;
      case AVAudioSessionPort.builtInMic:
        return fallback.isEmpty ? l10n.audioRouteReceiver : fallback;
      case null:
      default:
        return fallback.isEmpty ? l10n.audioRouteUnknown : fallback;
    }
  }

  static IconData portIcon(AVAudioSessionPort? type) {
    switch (type) {
      case AVAudioSessionPort.builtInSpeaker:
        return Icons.volume_up;
      case AVAudioSessionPort.builtInReceiver:
        return Icons.phone_in_talk;
      case AVAudioSessionPort.bluetoothHfp:
      case AVAudioSessionPort.bluetoothA2dp:
      case AVAudioSessionPort.bluetoothLe:
        return Icons.bluetooth_audio;
      case AVAudioSessionPort.headphones:
      case AVAudioSessionPort.headsetMic:
        return Icons.headset;
      case AVAudioSessionPort.carAudio:
        return Icons.directions_car;
      case AVAudioSessionPort.airPlay:
        return Icons.airplay;
      default:
        return Icons.speaker;
    }
  }

  Future<void> _openPicker() async {
    if (!kIsWeb && Platform.isAndroid) {
      final selectedDeviceId = await showModalBottomSheet<String>(
        context: context,
        showDragHandle: true,
        isScrollControlled: true,
        builder: (ctx) => _AndroidAudioOutputPickerSheet(
          devices: _androidDevices,
          loading: _androidLoading,
          onRefresh: _refreshAndroidDevices,
        ),
      );
      if (selectedDeviceId == null) return;
      try {
        if (selectedDeviceId == 'auto') {
          await _androidOutputChannel.invokeMethod<void>(
            'clearCommunicationDevice',
          );
        } else {
          final changed = await _androidOutputChannel.invokeMethod<bool>(
            'setCommunicationDevice',
            {'deviceId': selectedDeviceId},
          );
          if (changed != true && mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(AppL10n.of(context).audioRouteCannotSelect),
              ),
            );
          }
        }
        await _refreshAndroidDevices();
      } catch (_) {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(AppL10n.of(context).audioRouteChangeFailed)),
        );
      }
      return;
    }

    await showModalBottomSheet<void>(
      context: context,
      showDragHandle: true,
      isScrollControlled: true,
      builder: (ctx) => const _AudioOutputPickerSheet(),
    );
    await _refresh();
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppL10n.of(context);
    if (!kIsWeb && Platform.isAndroid) {
      final selected = selectedAndroidAudioOutputDevice(_androidDevices);
      final label = selected == null
          ? l10n.audioRouteSystemDefault
          : _androidDeviceLabel(selected.type, selected.name, l10n);
      return _AudioRouteRow(
        icon: _androidDeviceIcon(selected?.type),
        label: _androidLoading ? '${l10n.audioRouteUnknown}…' : label,
        onTap: _openPicker,
      );
    }

    final port = _activeOutput;
    final label = portLabel(port?.portType, port?.portName ?? '', l10n);
    return _AudioRouteRow(
      icon: portIcon(port?.portType),
      label: label,
      onTap: _openPicker,
    );
  }

  static String _androidDeviceLabel(
    String type,
    String fallback,
    AppL10n l10n,
  ) => switch (type) {
    'speaker' => l10n.audioRouteSpeaker,
    'earpiece' => l10n.audioRouteEarpiece,
    'wiredHeadset' || 'wiredHeadphones' => l10n.audioRouteWiredHeadset,
    'bluetoothA2dp' ||
    'bluetoothSco' ||
    'bluetoothLe' => l10n.audioRouteBluetooth,
    'usbHeadset' => fallback.isEmpty ? l10n.audioRouteUsbHeadset : fallback,
    'hdmi' => l10n.audioRouteCarAudio,
    _ => fallback.isEmpty ? l10n.audioRouteOtherDevice : fallback,
  };

  static IconData _androidDeviceIcon(String? type) => switch (type) {
    'speaker' => Icons.volume_up,
    'earpiece' => Icons.phone_in_talk,
    'wiredHeadset' || 'wiredHeadphones' || 'usbHeadset' => Icons.headset,
    'bluetoothA2dp' || 'bluetoothSco' || 'bluetoothLe' => Icons.bluetooth_audio,
    'hdmi' => Icons.tv,
    _ => Icons.speaker,
  };
}

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

  final IconData icon;
  final String label;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppL10n.of(context);
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
        child: Row(
          children: [
            Icon(icon, color: theme.colorScheme.primary),
            const SizedBox(width: 14),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    l10n.audioOutputLabel,
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                  Text(
                    label,
                    style: theme.textTheme.bodyLarge?.copyWith(
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ],
              ),
            ),
            Icon(
              Icons.chevron_right,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ],
        ),
      ),
    );
  }
}

class _AndroidAudioOutputPickerSheet extends StatelessWidget {
  const _AndroidAudioOutputPickerSheet({
    required this.devices,
    required this.loading,
    required this.onRefresh,
  });

  final List<AndroidAudioOutputDevice> devices;
  final bool loading;
  final Future<void> Function() onRefresh;

  @override
  Widget build(BuildContext context) {
    final l10n = AppL10n.of(context);
    final theme = Theme.of(context);
    final maxHeight = MediaQuery.sizeOf(context).height * 0.72;
    return SafeArea(
      child: ConstrainedBox(
        constraints: BoxConstraints(maxHeight: maxHeight),
        child: SingleChildScrollView(
          padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(l10n.audioOutputLabel, style: theme.textTheme.titleLarge),
              const SizedBox(height: 16),
              _PickerRow(
                icon: Icons.speaker,
                label: l10n.audioRouteSystemDefault,
                selected: !devices.any((d) => d.isSelected),
                onTap: () => Navigator.of(context).pop('auto'),
              ),
              if (loading)
                const Padding(
                  padding: EdgeInsets.symmetric(vertical: 24),
                  child: Center(child: CircularProgressIndicator()),
                )
              else if (devices.isEmpty)
                TextButton.icon(
                  onPressed: onRefresh,
                  icon: const Icon(Icons.refresh),
                  label: Text(l10n.audioRouteRefreshDevices),
                )
              else
                for (final device in devices)
                  _PickerRow(
                    icon: AudioOutputTileState._androidDeviceIcon(device.type),
                    label: AudioOutputTileState._androidDeviceLabel(
                      device.type,
                      device.name,
                      l10n,
                    ),
                    selected: device.isSelected,
                    onTap: device.isAvailableForCommunication
                        ? () => Navigator.of(context).pop(device.id)
                        : null,
                  ),
            ],
          ),
        ),
      ),
    );
  }
}

class _AudioOutputPickerSheet extends StatefulWidget {
  const _AudioOutputPickerSheet();

  @override
  State<_AudioOutputPickerSheet> createState() =>
      _AudioOutputPickerSheetState();
}

class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
  Set<AVAudioSessionPortDescription> _availableInputs = const {};
  AVAudioSessionRouteDescription? _route;
  StreamSubscription<AVAudioSessionRouteChange>? _routeSub;
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    _refresh();
    _routeSub = AVAudioSession().routeChangeStream.listen((_) {
      if (!mounted) return;
      _refresh();
    });
  }

  @override
  void dispose() {
    _routeSub?.cancel();
    _routeSub = null;
    super.dispose();
  }

  Future<void> _refresh() async {
    try {
      final session = AVAudioSession();
      final inputs = await session.availableInputs;
      final route = await session.currentRoute;
      if (!mounted) return;
      setState(() {
        _availableInputs = inputs;
        _route = route;
        _loading = false;
      });
    } catch (_) {
      if (!mounted) return;
      setState(() => _loading = false);
    }
  }

  Future<void> _selectSpeaker() async {
    try {
      await AVAudioSession().overrideOutputAudioPort(
        AVAudioSessionPortOverride.speaker,
      );
      unawaited(Haptics.selection().catchError((_) {}));
      debugPrint('chanora: audio output -> speakerphone (override applied)');
    } catch (e, st) {
      debugPrint('chanora: _selectSpeaker FAILED: $e\n$st');
    }
    if (!mounted) return;
    Navigator.of(context).pop();
  }

  Future<void> _selectReceiver() async {
    try {
      await AVAudioSession().overrideOutputAudioPort(
        AVAudioSessionPortOverride.none,
      );
      unawaited(Haptics.selection().catchError((_) {}));
      debugPrint('chanora: audio output -> receiver (override cleared)');
    } catch (e, st) {
      debugPrint('chanora: _selectReceiver FAILED: $e\n$st');
    }
    if (!mounted) return;
    Navigator.of(context).pop();
  }

  Future<void> _selectInput(AVAudioSessionPortDescription port) async {
    try {
      await AVAudioSession().overrideOutputAudioPort(
        AVAudioSessionPortOverride.none,
      );
      await AVAudioSession().setPreferredInput(port);
      unawaited(Haptics.selection().catchError((_) {}));
      debugPrint('chanora: audio output -> ${port.portType}');
    } catch (e, st) {
      debugPrint('chanora: _selectInput FAILED: $e\n$st');
    }
    if (!mounted) return;
    Navigator.of(context).pop();
  }

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

    if (_loading) {
      return const SafeArea(
        child: Padding(
          padding: EdgeInsets.all(40),
          child: Center(child: CircularProgressIndicator()),
        ),
      );
    }

    final currentOutputType = _route?.outputs.firstOrNull?.portType;
    final currentInputUid = _route?.inputs.firstOrNull?.uid;
    final isSpeaker = currentOutputType == AVAudioSessionPort.builtInSpeaker;
    final isReceiver = currentOutputType == AVAudioSessionPort.builtInReceiver;
    final externalInputs = _availableInputs
        .where((p) => p.portType != AVAudioSessionPort.builtInMic)
        .toList();

    return SafeArea(
      child: SingleChildScrollView(
        padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(l10n.audioOutputLabel, style: theme.textTheme.titleLarge),
            const SizedBox(height: 16),
            _PickerRow(
              icon: Icons.volume_up,
              label: l10n.audioRouteSpeaker,
              selected: isSpeaker,
              onTap: _selectSpeaker,
            ),
            _PickerRow(
              icon: Icons.phone_in_talk,
              label: l10n.audioRouteReceiver,
              selected: isReceiver,
              onTap: _selectReceiver,
            ),
            for (final port in externalInputs)
              _PickerRow(
                icon: AudioOutputTileState.portIcon(port.portType),
                label: AudioOutputTileState.portLabel(
                  port.portType,
                  port.portName,
                  l10n,
                ),
                selected: !isSpeaker && port.uid == currentInputUid,
                onTap: () => _selectInput(port),
              ),
          ],
        ),
      ),
    );
  }
}

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

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

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final enabled = onTap != null;
    final color = selected
        ? theme.colorScheme.primary
        : enabled
        ? theme.colorScheme.onSurface
        : theme.colorScheme.onSurfaceVariant.withAlpha(130);
    return Semantics(
      button: true,
      selected: selected,
      enabled: enabled,
      label: label,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(8),
        child: ExcludeSemantics(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 4),
            child: Row(
              children: [
                Icon(icon, color: color),
                const SizedBox(width: 16),
                Expanded(
                  child: Text(
                    label,
                    style: theme.textTheme.bodyLarge?.copyWith(color: color),
                  ),
                ),
                if (selected)
                  Icon(Icons.check, color: theme.colorScheme.primary),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
