import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:record/record.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../../core/audio/pcm_player.dart';
import '../../../../core/audio/speech_enhancer.dart';
import '../../../../core/config/api_endpoints.dart';
import '../../../../core/config/app_config.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/theme/app_colors.dart';

/// Tracks the current state of the voice call.
enum _CallPhase { ringing, connecting, active, ended }

class AgentCallPage extends ConsumerStatefulWidget {
  const AgentCallPage({super.key});

  @override
  ConsumerState<AgentCallPage> createState() => _AgentCallPageState();
}

class _AgentCallPageState extends ConsumerState<AgentCallPage>
    with SingleTickerProviderStateMixin {
  _CallPhase _phase = _CallPhase.ringing;
  String? _errorMessage;
  String? _sessionId;
  WebSocketChannel? _audioChannel;
  StreamSubscription? _audioSubscription;

  // Audio capture
  late final AudioRecorder _recorder;
  final SpeechEnhancer _enhancer = SpeechEnhancer();
  StreamSubscription<List<int>>? _micSubscription;

  // Audio playback
  final PcmPlayer _pcmPlayer = PcmPlayer();

  // Call duration
  final Stopwatch _stopwatch = Stopwatch();
  Timer? _durationTimer;
  String _durationLabel = '00:00';

  // Waveform
  late AnimationController _waveController;
  final List<double> _waveHeights = List.generate(20, (_) => 0.3);

  // Mute / speaker state
  bool _isMuted = false;
  bool _isSpeakerOn = false;

  // Reconnection
  int _reconnectAttempts = 0;
  static const int _maxReconnectAttempts = 5;
  Timer? _reconnectTimer;
  Timer? _heartbeatCheckTimer;
  DateTime? _lastServerPing;
  bool _isReconnecting = false;
  bool _userEndedCall = false;

  @override
  void initState() {
    super.initState();
    _recorder = AudioRecorder();
    _enhancer.init();
    _waveController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
  }

  // ---------------------------------------------------------------------------
  // Call lifecycle
  // ---------------------------------------------------------------------------

  /// Accept call: create voice session, connect WebSocket, start mic + player.
  Future<void> _acceptCall() async {
    setState(() {
      _phase = _CallPhase.connecting;
      _errorMessage = null;
    });

    try {
      final dio = ref.read(dioClientProvider);
      final config = ref.read(appConfigProvider);
      final response = await dio.post('${ApiEndpoints.voice}/sessions', data: {});
      final data = response.data as Map<String, dynamic>;

      _sessionId = data['session_id'] as String? ?? data['sessionId'] as String? ?? data['id'] as String?;

      // Build WebSocket URL — prefer backend-returned path
      String wsUrl;
      final backendWsUrl =
          data['websocket_url'] as String? ?? data['ws_url'] as String?;
      if (backendWsUrl != null && backendWsUrl.startsWith('/')) {
        // Relative path from backend — prepend wsBaseUrl host
        final uri = Uri.parse(config.wsBaseUrl);
        wsUrl = '${uri.scheme}://${uri.host}:${uri.port}$backendWsUrl';
      } else if (backendWsUrl != null) {
        wsUrl = backendWsUrl;
      } else {
        wsUrl = '${config.wsBaseUrl}/api/v1/voice/ws/$_sessionId';
      }

      // Connect WebSocket
      _audioChannel = WebSocketChannel.connect(Uri.parse(wsUrl));

      // Listen for incoming data (text control messages + binary audio)
      _audioSubscription = _audioChannel!.stream.listen(
        _onWsData,
        onDone: () => _onWsDisconnected(),
        onError: (_) => _onWsDisconnected(),
      );

      // Start bidirectional heartbeat
      _startHeartbeat();

      // Initialize audio playback
      await _pcmPlayer.init();

      // Start mic capture
      await _startMicCapture();

      // Start duration timer
      _stopwatch.start();
      _durationTimer = Timer.periodic(const Duration(seconds: 1), (_) {
        if (!mounted) return;
        final elapsed = _stopwatch.elapsed;
        setState(() {
          _durationLabel =
              '${elapsed.inMinutes.toString().padLeft(2, '0')}:${(elapsed.inSeconds % 60).toString().padLeft(2, '0')}';
        });
      });

      _waveController.repeat(reverse: true);

      setState(() => _phase = _CallPhase.active);
    } catch (e) {
      if (mounted) {
        setState(() {
          _phase = _CallPhase.ringing;
          _errorMessage = '连接失败: ${_friendlyError(e)}';
        });
      }
    }
  }

  /// Start real microphone capture: record PCM 16kHz mono, denoise, send over WS.
  Future<void> _startMicCapture() async {
    final hasPermission = await _recorder.hasPermission();
    if (!hasPermission) return;

    final stream = await _recorder.startStream(const RecordConfig(
      encoder: AudioEncoder.pcm16bits,
      sampleRate: 16000,
      numChannels: 1,
      noiseSuppress: true,
      autoGain: true,
    ));

    _micSubscription = stream.listen((chunk) {
      if (_phase != _CallPhase.active || _isMuted) return;
      try {
        // Denoise each chunk with GTCRN
        final pcm = Uint8List.fromList(chunk);
        final denoised = _enhancer.enhance(pcm);
        _audioChannel?.sink.add(denoised);
      } catch (_) {}
    });
  }

  // ---------------------------------------------------------------------------
  // WebSocket data routing & heartbeat
  // ---------------------------------------------------------------------------

  /// Top-level handler that distinguishes text (JSON control) from binary
  /// (audio) WebSocket frames.
  void _onWsData(dynamic data) {
    if (data is String) {
      _handleControlMessage(data);
    } else {
      _onAudioReceived(data);
    }
  }

  /// Decode a JSON control frame from the server and respond accordingly.
  void _handleControlMessage(String jsonStr) {
    try {
      final msg = jsonDecode(jsonStr) as Map<String, dynamic>;
      final type = msg['type'] as String?;
      switch (type) {
        case 'ping':
          // Server heartbeat — update liveness timestamp.
          // Note: We don't send pong back because Pipecat owns the server-side
          // WebSocket read loop and would intercept the text frame.
          _lastServerPing = DateTime.now();
          break;
        case 'session.resumed':
          // Reconnection confirmed by server
          _lastServerPing = DateTime.now();
          break;
        case 'session.ended':
          _onCallEnded();
          break;
        case 'error':
          if (mounted) {
            final detail = msg['message'] as String? ?? 'Unknown error';
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('服务端错误: $detail'),
                backgroundColor: AppColors.error,
              ),
            );
          }
          break;
      }
    } catch (_) {
      // Malformed JSON — ignore
    }
  }

  /// Start monitoring server liveness based on incoming pings.
  ///
  /// The server sends `{"type": "ping"}` every 15 s.  If we haven't received
  /// one in 45 s (3 missed pings), we assume the connection is dead and
  /// trigger a reconnect.
  void _startHeartbeat() {
    _heartbeatCheckTimer?.cancel();

    // Mark initial timestamp so the first 45-second window starts now
    _lastServerPing = DateTime.now();

    // Periodically verify server is alive
    _heartbeatCheckTimer = Timer.periodic(const Duration(seconds: 10), (_) {
      if (_lastServerPing != null &&
          DateTime.now().difference(_lastServerPing!).inSeconds > 45) {
        _triggerReconnect();
      }
    });
  }

  /// Cancel heartbeat check timer.
  void _stopHeartbeat() {
    _heartbeatCheckTimer?.cancel();
    _heartbeatCheckTimer = null;
  }

  // ---------------------------------------------------------------------------
  // Auto-reconnect with exponential backoff
  // ---------------------------------------------------------------------------

  /// Called when the WebSocket drops unexpectedly (not user-initiated).
  void _onWsDisconnected() {
    if (_phase == _CallPhase.ended || _isReconnecting || _userEndedCall) return;
    _triggerReconnect();
  }

  /// Attempt to reconnect using exponential backoff (1 s, 2 s, 4 s, 8 s,
  /// 16 s) with a maximum of [_maxReconnectAttempts] tries.
  Future<void> _triggerReconnect() async {
    if (_isReconnecting || _phase == _CallPhase.ended || _userEndedCall) return;
    _isReconnecting = true;
    if (mounted) setState(() {});

    // Tear down current connection & heartbeat
    _stopHeartbeat();
    await _audioSubscription?.cancel();
    _audioSubscription = null;
    try {
      await _audioChannel?.sink.close();
    } catch (_) {}
    _audioChannel = null;

    for (int attempt = 0; attempt < _maxReconnectAttempts; attempt++) {
      _reconnectAttempts = attempt + 1;
      if (!mounted || _phase == _CallPhase.ended || _userEndedCall) break;

      final delaySecs = min(pow(2, attempt).toInt(), 16);
      await Future.delayed(Duration(seconds: delaySecs));

      if (!mounted || _phase == _CallPhase.ended || _userEndedCall) break;

      try {
        // Ask backend if session is still alive
        final dio = ref.read(dioClientProvider);
        final response = await dio.post(
          '${ApiEndpoints.voice}/sessions/$_sessionId/reconnect',
        );
        final data = response.data as Map<String, dynamic>;

        // Build new WebSocket URL
        final config = ref.read(appConfigProvider);
        String wsUrl;
        final backendWsUrl = data['websocket_url'] as String?;
        if (backendWsUrl != null && backendWsUrl.startsWith('/')) {
          final uri = Uri.parse(config.wsBaseUrl);
          wsUrl = '${uri.scheme}://${uri.host}:${uri.port}$backendWsUrl';
        } else if (backendWsUrl != null) {
          wsUrl = backendWsUrl;
        } else {
          wsUrl = '${config.wsBaseUrl}/api/v1/voice/ws/$_sessionId';
        }

        // Connect new WebSocket
        _audioChannel = WebSocketChannel.connect(Uri.parse(wsUrl));
        _audioSubscription = _audioChannel!.stream.listen(
          _onWsData,
          onDone: () => _onWsDisconnected(),
          onError: (_) => _onWsDisconnected(),
        );

        // Reset audio jitter buffer for a fresh start
        _pcmPlayer.reset();

        // Restart heartbeat
        _startHeartbeat();

        _reconnectAttempts = 0;
        _isReconnecting = false;
        if (mounted) setState(() {});
        return; // success
      } catch (_) {
        // Will retry on next iteration
      }
    }

    // All reconnection attempts exhausted
    _isReconnecting = false;
    _onCallEnded();
  }

  /// Handle incoming audio from the agent side.
  void _onAudioReceived(dynamic data) {
    if (!mounted || _phase != _CallPhase.active) return;

    Uint8List pcmData;
    if (data is Uint8List) {
      pcmData = data;
    } else if (data is List<int>) {
      pcmData = Uint8List.fromList(data);
    } else {
      return;
    }

    // Feed to player for real-time playback
    _pcmPlayer.feed(pcmData);

    // Drive waveform from actual audio energy (RMS)
    _updateWaveform(pcmData);
  }

  /// Calculate RMS from PCM data and update waveform bars.
  void _updateWaveform(Uint8List pcmData) {
    if (pcmData.length < 4) return;
    final byteData = ByteData.sublistView(pcmData);
    final sampleCount = pcmData.length ~/ 2;

    // Compute overall RMS
    double sum = 0;
    for (int i = 0; i < sampleCount; i++) {
      final s = byteData.getInt16(i * 2, Endian.little).toDouble();
      sum += s * s;
    }
    final rms = sqrt(sum / sampleCount) / 32768.0; // normalize to [0,1]

    setState(() {
      final rand = Random();
      for (int i = 0; i < _waveHeights.length; i++) {
        _waveHeights[i] =
            (rms * 2.0 + rand.nextDouble() * 0.2).clamp(0.1, 1.0);
      }
    });
  }

  /// End the call and clean up all resources.
  Future<void> _endCall() async {
    _userEndedCall = true;
    _isReconnecting = false;
    setState(() => _phase = _CallPhase.ended);
    _stopwatch.stop();
    _durationTimer?.cancel();
    _waveController.stop();

    // Cancel heartbeat & reconnect timers
    _stopHeartbeat();
    _reconnectTimer?.cancel();
    _reconnectTimer = null;

    // Cleanup all resources — each wrapped in try/catch to ensure we always
    // reach Navigator.pop() at the end.
    try {
      await _micSubscription?.cancel();
      _micSubscription = null;
    } catch (_) {}
    try {
      await _recorder.stop();
    } catch (_) {}
    try {
      await _pcmPlayer.dispose();
    } catch (_) {}
    try {
      _audioSubscription?.cancel();
    } catch (_) {}
    try {
      await _audioChannel?.sink.close();
    } catch (_) {}

    // Delete voice session on the server (fire-and-forget)
    if (_sessionId != null) {
      try {
        final dio = ref.read(dioClientProvider);
        await dio.delete('${ApiEndpoints.voice}/sessions/$_sessionId')
            .timeout(const Duration(seconds: 3));
      } catch (_) {}
    }

    // Navigate back to the dial page
    if (mounted) {
      await Future.delayed(const Duration(milliseconds: 500));
      if (mounted) Navigator.of(context).pop();
    }
  }

  void _onCallEnded() {
    if (_phase == _CallPhase.ended) return;
    _endCall();
  }

  // ---------------------------------------------------------------------------
  // Build
  // ---------------------------------------------------------------------------

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppColors.background,
      body: SafeArea(
        child: Stack(
          children: [
            Center(
              child: Column(
                children: [
                  const Spacer(flex: 2),
                  _buildAvatar(),
                  const SizedBox(height: 24),
                  Text(
                    _statusText,
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                        fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    _subtitleText,
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                        color: AppColors.textSecondary, fontSize: 15),
                  ),
                  // Inline error message
                  if (_errorMessage != null) ...[
                    const SizedBox(height: 16),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 32),
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 16, vertical: 12),
                        decoration: BoxDecoration(
                          color: AppColors.error.withOpacity(0.1),
                          borderRadius: BorderRadius.circular(12),
                          border: Border.all(
                              color: AppColors.error.withOpacity(0.3)),
                        ),
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Icon(Icons.error_outline,
                                size: 18, color: AppColors.error),
                            const SizedBox(width: 8),
                            Flexible(
                              child: Text(
                                _errorMessage!,
                                textAlign: TextAlign.center,
                                style: const TextStyle(
                                  color: AppColors.error,
                                  fontSize: 13,
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                  const SizedBox(height: 32),
                  if (_phase == _CallPhase.active)
                    Text(
                      _durationLabel,
                      style: const TextStyle(
                        fontSize: 40,
                        fontWeight: FontWeight.w300,
                        color: AppColors.textPrimary,
                        letterSpacing: 4,
                      ),
                    ),
                  const SizedBox(height: 24),
                  if (_phase == _CallPhase.active) _buildWaveform(),
                  const Spacer(flex: 3),
                  _buildControls(),
                  const SizedBox(height: 48),
                ],
              ),
            ),
            // Reconnecting overlay
            if (_isReconnecting)
              Positioned(
                top: 0,
                left: 0,
                right: 0,
                child: Container(
                  padding:
                      const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
                  color: AppColors.warning.withOpacity(0.9),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          color: Colors.white,
                        ),
                      ),
                      const SizedBox(width: 10),
                      Text(
                        '重新连接中... ($_reconnectAttempts/$_maxReconnectAttempts)',
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 14,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  String get _statusText {
    switch (_phase) {
      case _CallPhase.ringing:
        return 'iAgent 语音通话';
      case _CallPhase.connecting:
        return '连接中...';
      case _CallPhase.active:
        return 'iAgent';
      case _CallPhase.ended:
        return '通话结束';
    }
  }

  String get _subtitleText {
    if (_errorMessage != null && _phase == _CallPhase.ringing) {
      return '';
    }
    switch (_phase) {
      case _CallPhase.ringing:
        return '智能运维语音助手';
      case _CallPhase.connecting:
        return '正在建立安全连接';
      case _CallPhase.active:
        return '语音通话中';
      case _CallPhase.ended:
        return _durationLabel;
    }
  }

  /// Extract a short user-friendly message from an exception.
  String _friendlyError(dynamic e) {
    final s = e.toString();
    // Extract status code info for Dio errors
    final match = RegExp(r'status code of (\d+)').firstMatch(s);
    if (match != null) {
      final code = match.group(1);
      switch (code) {
        case '503':
          return '语音服务暂不可用 ($code)';
        case '502':
          return '语音服务网关错误 ($code)';
        case '404':
          return '语音服务未找到 ($code)';
        default:
          return '服务器错误 ($code)';
      }
    }
    if (s.contains('SocketException') || s.contains('Connection refused')) {
      return '无法连接到服务器';
    }
    if (s.length > 80) return '${s.substring(0, 80)}...';
    return s;
  }

  Widget _buildAvatar() {
    final isActive = _phase == _CallPhase.active;
    return AnimatedContainer(
      duration: const Duration(milliseconds: 400),
      width: isActive ? 100 : 120,
      height: isActive ? 100 : 120,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: AppColors.primary.withOpacity(0.15),
        border: Border.all(
          color: isActive ? AppColors.success : AppColors.primary,
          width: 3,
        ),
        boxShadow: isActive
            ? [
                BoxShadow(
                  color: AppColors.success.withOpacity(0.3),
                  blurRadius: 24,
                  spreadRadius: 4,
                ),
              ]
            : [],
      ),
      child: Icon(
        Icons.smart_toy,
        size: isActive ? 48 : 56,
        color: isActive ? AppColors.success : AppColors.primary,
      ),
    );
  }

  Widget _buildWaveform() {
    return SizedBox(
      height: 60,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: List.generate(_waveHeights.length, (i) {
          return AnimatedContainer(
            duration: const Duration(milliseconds: 120),
            width: 4,
            height: 10 + _waveHeights[i] * 50,
            margin: const EdgeInsets.symmetric(horizontal: 2),
            decoration: BoxDecoration(
              color: AppColors.primary
                  .withOpacity(0.6 + _waveHeights[i] * 0.4),
              borderRadius: BorderRadius.circular(2),
            ),
          );
        }),
      ),
    );
  }

  Widget _buildControls() {
    switch (_phase) {
      case _CallPhase.ringing:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FloatingActionButton(
              heroTag: 'decline',
              backgroundColor: AppColors.error,
              onPressed: () => Navigator.of(context).pop(),
              child: const Icon(Icons.call_end, color: Colors.white),
            ),
            const SizedBox(width: 40),
            FloatingActionButton(
              heroTag: 'accept',
              backgroundColor: AppColors.success,
              onPressed: _acceptCall,
              child: const Icon(Icons.call, color: Colors.white),
            ),
          ],
        );

      case _CallPhase.connecting:
        return const CircularProgressIndicator();

      case _CallPhase.active:
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _CircleButton(
              icon: _isMuted ? Icons.mic_off : Icons.mic,
              label: _isMuted ? '取消静音' : '静音',
              isActive: _isMuted,
              onTap: () => setState(() => _isMuted = !_isMuted),
            ),
            const SizedBox(width: 24),
            FloatingActionButton(
              heroTag: 'end',
              backgroundColor: AppColors.error,
              onPressed: _endCall,
              child: const Icon(Icons.call_end, color: Colors.white),
            ),
            const SizedBox(width: 24),
            _CircleButton(
              icon: _isSpeakerOn ? Icons.volume_up : Icons.volume_down,
              label: '扬声器',
              isActive: _isSpeakerOn,
              onTap: () {
                setState(() => _isSpeakerOn = !_isSpeakerOn);
                _pcmPlayer.setSpeakerOn(_isSpeakerOn);
              },
            ),
          ],
        );

      case _CallPhase.ended:
        return const Icon(Icons.call_end,
            size: 48, color: AppColors.textMuted);
    }
  }

  @override
  void dispose() {
    _userEndedCall = true;
    _durationTimer?.cancel();
    _stopHeartbeat();
    _reconnectTimer?.cancel();
    _waveController.dispose();
    _stopwatch.stop();
    _micSubscription?.cancel();
    _recorder.dispose();
    _enhancer.dispose();
    _audioSubscription?.cancel();
    _audioChannel?.sink.close();
    _pcmPlayer.dispose();
    super.dispose();
  }
}

// ---------------------------------------------------------------------------
// Small helper widget for mute / speaker buttons
// ---------------------------------------------------------------------------

class _CircleButton extends StatelessWidget {
  final IconData icon;
  final String label;
  final bool isActive;
  final VoidCallback onTap;

  const _CircleButton({
    required this.icon,
    required this.label,
    required this.isActive,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        GestureDetector(
          onTap: onTap,
          child: Container(
            width: 52,
            height: 52,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: isActive
                  ? AppColors.textPrimary.withOpacity(0.2)
                  : AppColors.surfaceLight,
            ),
            child: Icon(icon, color: AppColors.textPrimary),
          ),
        ),
        const SizedBox(height: 6),
        Text(
          label,
          style:
              const TextStyle(color: AppColors.textSecondary, fontSize: 11),
        ),
      ],
    );
  }
}
