part of '../../main.dart';

class ConfiVoiceShell extends StatefulWidget {
  const ConfiVoiceShell({
    super.key,
    required this.session,
    required this.onLogout,
  });

  final UserSession session;
  final Future<void> Function() onLogout;

  @override
  State<ConfiVoiceShell> createState() => _ConfiVoiceShellState();
}

class _ConfiVoiceShellState extends State<ConfiVoiceShell> {
  static const _apiEndpointPreferenceKey = 'confivoice_api_endpoint';
  static const supportedExtensions = [
    'wav',
    'mp3',
    'm4a',
    'ogg',
    'flac',
    'webm',
    'aac',
  ];

  final TextEditingController _apiController = TextEditingController(
    text: defaultApiEndpoint(),
  );
  final TextEditingController _studentNameController = TextEditingController();
  final AudioRecorder _recorder = AudioRecorder();
  StreamSubscription<Amplitude>? _amplitudeSubscription;

  SelectedAudio? _selectedAudio;
  PredictionResult? _prediction;
  String? _errorMessage;
  bool _isPredicting = false;
  bool _isSaving = false;
  bool _isRecording = false;
  String _studentGender = '';
  List<String> _studentNames = const [];
  Map<String, String> _studentGenders = const {};
  List<double> _liveWavePreview = const [];
  List<double> _wavePreview = const [];

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

  @override
  void dispose() {
    _amplitudeSubscription?.cancel();
    _apiController.dispose();
    _studentNameController.dispose();
    _recorder.dispose();
    super.dispose();
  }

  Future<void> _loadApiEndpoint() async {
    try {
      final preferences = await SharedPreferences.getInstance();
      final endpoint = normalizeApiEndpoint(
        preferences.getString(_apiEndpointPreferenceKey) ?? '',
      );
      if (endpoint.isNotEmpty && mounted) {
        setState(() {
          _apiController.text = endpoint;
        });
      }
    } catch (_) {
      // Gunakan endpoint default jika storage lokal belum dapat dibaca.
    }
  }

  Future<void> _saveApiEndpoint() async {
    final preferences = await SharedPreferences.getInstance();
    await preferences.setString(
      _apiEndpointPreferenceKey,
      _apiController.text.trim(),
    );
  }

  String get _studentNamesPreferenceKey => 'confivoice_student_names_shared';
  String get _studentGendersPreferenceKey =>
      'confivoice_student_genders_shared';

  Future<void> _initializeShell() async {
    await _loadApiEndpoint();
    await _loadStudentNames();
  }

  Future<void> _loadStudentNames() async {
    try {
      final preferences = await SharedPreferences.getInstance();
      final localNames =
          preferences.getStringList(_studentNamesPreferenceKey) ?? [];
      final localGenderJson =
          preferences.getString(_studentGendersPreferenceKey) ?? '{}';
      final localGenders = _decodeStudentGenders(localGenderJson);
      final savedResultData = await _loadStudentDataFromSavedResults();
      final names = _normalizeStudentNames([
        ...localNames,
        ...savedResultData.names,
      ]);
      final genders = _mergeStudentGenders(
        localGenders,
        savedResultData.genders,
      );
      await preferences.setStringList(_studentNamesPreferenceKey, names);
      await preferences.setString(
        _studentGendersPreferenceKey,
        jsonEncode(genders),
      );
      if (!mounted) {
        return;
      }
      setState(() {
        _studentNames = names;
        _studentGenders = genders;
      });
    } catch (_) {}
  }

  Future<_StudentData> _loadStudentDataFromSavedResults() async {
    try {
      final response = await http
          .get(_apiEndpoint('/predictions'))
          .timeout(const Duration(seconds: 12));

      if (response.statusCode < 200 || response.statusCode >= 300) {
        return const _StudentData(names: [], genders: {});
      }

      final decoded = jsonDecode(response.body) as Map<String, dynamic>;
      final data = decoded['data'] as List<dynamic>? ?? [];
      final names = <String>[];
      final genders = <String, String>{};
      for (final item in data) {
        final row = (item as Map).cast<String, dynamic>();
        final name = _cleanStudentName(row['student_name']?.toString() ?? '');
        final gender = _cleanStudentGender(
          row['student_gender']?.toString() ?? '',
        );
        if (name.isEmpty) {
          continue;
        }
        names.add(name);
        if (gender.isNotEmpty) {
          genders[name.toLowerCase()] = gender;
        }
      }
      return _StudentData(names: names, genders: genders);
    } catch (_) {
      return const _StudentData(names: [], genders: {});
    }
  }

  Map<String, String> _decodeStudentGenders(String rawJson) {
    try {
      final decoded = jsonDecode(rawJson) as Map<String, dynamic>;
      return decoded.map(
        (key, value) =>
            MapEntry(key.toLowerCase(), _cleanStudentGender(value.toString())),
      )..removeWhere((_, value) => value.isEmpty);
    } catch (_) {
      return {};
    }
  }

  Map<String, String> _mergeStudentGenders(
    Map<String, String> localGenders,
    Map<String, String> savedGenders,
  ) {
    return {...savedGenders, ...localGenders}
      ..removeWhere((_, value) => value.isEmpty);
  }

  List<String> _normalizeStudentNames(Iterable<String> names) {
    final seen = <String>{};
    final normalized = <String>[];
    for (final name in names) {
      final cleanName = _cleanStudentName(name);
      if (cleanName.isEmpty) {
        continue;
      }
      final key = cleanName.toLowerCase();
      if (seen.add(key)) {
        normalized.add(cleanName);
      }
    }
    normalized.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
    return normalized;
  }

  String _cleanStudentName(String name) {
    return name
        .trim()
        .split(RegExp(r'\s+'))
        .where((part) => part.isNotEmpty)
        .join(' ');
  }

  String _cleanStudentGender(String gender) {
    final cleanGender = gender.trim();
    return cleanGender == 'Laki-laki' || cleanGender == 'Perempuan'
        ? cleanGender
        : '';
  }

  Future<void> _rememberStudentName(String name, {String? gender}) async {
    final cleanName = _cleanStudentName(name);
    if (cleanName.isEmpty) {
      return;
    }

    final cleanGender = _cleanStudentGender(gender ?? _studentGender);
    final updated = _normalizeStudentNames([..._studentNames, cleanName]);
    final updatedGenders = Map<String, String>.from(_studentGenders);
    if (cleanGender.isNotEmpty) {
      updatedGenders[cleanName.toLowerCase()] = cleanGender;
    }
    final preferences = await SharedPreferences.getInstance();
    await preferences.setStringList(_studentNamesPreferenceKey, updated);
    await preferences.setString(
      _studentGendersPreferenceKey,
      jsonEncode(updatedGenders),
    );
    if (mounted) {
      setState(() {
        _studentNames = updated;
        _studentGenders = updatedGenders;
        _studentNameController.text = cleanName;
        if (cleanGender.isNotEmpty) {
          _studentGender = cleanGender;
        }
      });
    }
  }

  void _applyStudentGenderFromName(String name) {
    final gender = _studentGenders[_cleanStudentName(name).toLowerCase()];
    if (gender == null || gender.isEmpty) {
      return;
    }
    setState(() {
      _studentGender = gender;
      _prediction = null;
      _errorMessage = null;
    });
  }

  bool get _hasStoredGenderForCurrentStudent {
    final cleanName = _cleanStudentName(_studentNameController.text);
    if (cleanName.isEmpty) {
      return false;
    }
    return _studentGenders[cleanName.toLowerCase()]?.isNotEmpty == true;
  }

  Future<void> _pickAudio() async {
    final audio = await pickAudio(supportedExtensions);
    if (audio == null) {
      return;
    }

    final preview = await buildWavePreview(audio);

    setState(() {
      _selectedAudio = audio;
      _wavePreview = preview;
      _prediction = null;
      _errorMessage = null;
    });
  }

  Future<void> _toggleRecording() async {
    if (_isRecording) {
      await _stopRecording();
    } else {
      await _startRecording();
    }
  }

  Future<void> _startRecording() async {
    try {
      final hasPermission = await _recorder.hasPermission();
      if (!hasPermission) {
        setState(() {
          _errorMessage = 'Izin microphone belum diberikan.';
        });
        return;
      }

      final supportsWav = await _recorder.isEncoderSupported(AudioEncoder.wav);
      if (!supportsWav) {
        setState(() {
          _errorMessage = 'Perangkat ini belum mendukung rekaman WAV.';
        });
        return;
      }

      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final recordingPath = recordingFilePath(
        'confivoice_recording_$timestamp.wav',
      );

      await _recorder.start(
        const RecordConfig(
          encoder: AudioEncoder.wav,
          sampleRate: 22050,
          numChannels: 1,
          noiseSuppress: true,
        ),
        path: recordingPath,
      );

      await _amplitudeSubscription?.cancel();
      _amplitudeSubscription = _recorder
          .onAmplitudeChanged(const Duration(milliseconds: 90))
          .listen((amplitude) {
            if (!mounted || !_isRecording) {
              return;
            }
            final normalized = ((amplitude.current + 55) / 55).clamp(0.04, 1.0);
            final nextValues = [..._liveWavePreview, normalized.toDouble()];
            setState(() {
              _liveWavePreview =
                  nextValues.length > 48
                      ? nextValues.sublist(nextValues.length - 48)
                      : nextValues;
            });
          });

      setState(() {
        _isRecording = true;
        _selectedAudio = null;
        _liveWavePreview = List<double>.filled(24, 0.06);
        _wavePreview = const [];
        _prediction = null;
        _errorMessage = null;
      });
    } catch (error) {
      setState(() {
        _isRecording = false;
        _errorMessage = 'Gagal mulai rekam: $error';
      });
    }
  }

  Future<void> _stopRecording() async {
    try {
      await _amplitudeSubscription?.cancel();
      _amplitudeSubscription = null;
      final path = await _recorder.stop();
      if (path == null) {
        setState(() {
          _isRecording = false;
          _liveWavePreview = const [];
          _errorMessage = 'Rekaman gagal disimpan.';
        });
        return;
      }

      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final audio = await audioFromRecorderPath(
        path,
        'confivoice_recording_$timestamp.wav',
      );
      if (audio == null) {
        setState(() {
          _isRecording = false;
          _errorMessage = 'Rekaman gagal dibaca.';
        });
        return;
      }
      final preview = await buildWavePreview(audio);

      setState(() {
        _isRecording = false;
        _selectedAudio = audio;
        _liveWavePreview = const [];
        _wavePreview = preview;
        _prediction = null;
        _errorMessage = null;
      });
    } catch (error) {
      setState(() {
        _isRecording = false;
        _liveWavePreview = const [];
        _errorMessage = 'Gagal berhenti rekam: $error';
      });
    }
  }

  Future<void> _predictAudio() async {
    final audio = _selectedAudio;
    if (audio == null) {
      setState(() {
        _errorMessage = 'Pilih audio terlebih dahulu.';
      });
      return;
    }

    final studentName = _studentNameController.text.trim();
    if (studentName.isEmpty) {
      setState(() {
        _errorMessage = 'Isi nama siswa/i terlebih dahulu.';
      });
      return;
    }
    if (_studentGender.isEmpty) {
      setState(() {
        _errorMessage = 'Pilih jenis kelamin siswa/i terlebih dahulu.';
      });
      return;
    }

    final endpoint = Uri.tryParse(_apiController.text.trim());
    if (endpoint == null || !endpoint.hasScheme || endpoint.host.isEmpty) {
      setState(() {
        _errorMessage = 'Endpoint API tidak valid.';
      });
      return;
    }

    setState(() {
      _isPredicting = true;
      _errorMessage = null;
    });

    try {
      final request = http.MultipartRequest('POST', endpoint);
      request.fields['student_name'] = studentName;
      request.fields['student_gender'] = _studentGender;
      request.files.add(await multipartFileFromAudio('file', audio));

      final response = await request.send().timeout(
        const Duration(seconds: 60),
      );
      final responseBody = await response.stream.bytesToString();

      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw _PredictionApiException(
          response.statusCode,
          _readApiErrorMessage(responseBody),
        );
      }

      final decoded = jsonDecode(responseBody) as Map<String, dynamic>;
      final result = PredictionResult.fromJson(decoded);
      await _saveApiEndpoint();

      setState(() {
        _prediction = result;
      });
    } on _PredictionApiException catch (error) {
      setState(() {
        _errorMessage = 'Gagal prediksi: ${error.message}';
      });
    } catch (error) {
      setState(() {
        final endpointText = _apiController.text.trim();
        if (endpointText.contains('10.0.2.2')) {
          _errorMessage =
              'Gagal prediksi: endpoint 10.0.2.2 hanya untuk emulator Android. '
              'Jika memakai HP fisik, buka ikon pengaturan lalu ganti URL prediksi '
              'ke alamat IP komputer yang menjalankan API, misalnya '
              'http://192.168.1.6:8000/predict.';
        } else {
          _errorMessage =
              'Gagal prediksi: tidak bisa terhubung ke API. Pastikan API berjalan, '
              'HP dan komputer berada di WiFi yang sama, lalu cek URL di pengaturan. Detail: $error';
        }
      });
    } finally {
      if (mounted) {
        setState(() {
          _isPredicting = false;
        });
      }
    }
  }

  Future<void> _savePrediction() async {
    final prediction = _prediction;
    if (prediction == null || !prediction.isValidAudio) {
      setState(() {
        _errorMessage = 'Belum ada hasil analisis yang bisa disimpan.';
      });
      return;
    }

    final predictEndpoint = Uri.tryParse(_apiController.text.trim());
    if (predictEndpoint == null ||
        !predictEndpoint.hasScheme ||
        predictEndpoint.host.isEmpty) {
      setState(() {
        _errorMessage = 'Endpoint API tidak valid.';
      });
      return;
    }

    final saveEndpoint = _apiEndpoint('/predictions');

    setState(() {
      _isSaving = true;
      _errorMessage = null;
    });

    try {
      var response = await http
          .post(
            saveEndpoint,
            headers: {'Content-Type': 'application/json'},
            body: jsonEncode(
              prediction.toSaveJson(widget.session.id, _studentGender),
            ),
          )
          .timeout(const Duration(seconds: 30));

      if (response.statusCode == 405) {
        response = await http
            .post(
              _apiEndpoint('/save'),
              headers: {'Content-Type': 'application/json'},
              body: jsonEncode(
                prediction.toSaveJson(widget.session.id, _studentGender),
              ),
            )
            .timeout(const Duration(seconds: 30));
      }

      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw Exception('API error ${response.statusCode}: ${response.body}');
      }

      if (!mounted) {
        return;
      }

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Hasil analisis berhasil disimpan.')),
      );

      await _rememberStudentName(
        prediction.studentName,
        gender: _studentGender,
      );
      _studentNameController.clear();
      setState(() {
        _studentGender = '';
        _selectedAudio = null;
        _liveWavePreview = const [];
        _wavePreview = const [];
        _prediction = null;
      });
    } catch (error) {
      setState(() {
        _errorMessage = 'Gagal menyimpan hasil analisis: $error';
      });
    } finally {
      if (mounted) {
        setState(() {
          _isSaving = false;
        });
      }
    }
  }

  Uri _apiEndpoint(String path, {Map<String, String>? queryParameters}) {
    return Uri.parse(
      _apiController.text.trim(),
    ).replace(path: path, queryParameters: queryParameters);
  }

  void _showSavedAnalyses() {
    Navigator.push(
      context,
      MaterialPageRoute<void>(
        builder:
            (context) => SavedAnalysesPage(
              endpoint: _apiEndpoint(
                '/predictions',
                queryParameters: {'limit': '1000'},
              ),
            ),
      ),
    );
  }

  Future<void> _confirmLogout() async {
    final shouldLogout = await showDialog<bool>(
      context: context,
      builder:
          (context) => AlertDialog(
            backgroundColor: AppColors.surface,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(14),
              side: const BorderSide(color: AppColors.border),
            ),
            title: const Text('Logout?'),
            content: const Text('Sesi akun akan ditutup dari aplikasi ini.'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context, false),
                child: const Text('Batal'),
              ),
              FilledButton.icon(
                onPressed: () => Navigator.pop(context, true),
                icon: const Icon(Icons.logout, size: 15),
                label: const Text('Logout'),
              ),
            ],
          ),
    );

    if (shouldLogout == true) {
      await widget.onLogout();
    }
  }

  @override
  Widget build(BuildContext context) {
    final displayUserName =
        widget.session.fullName.trim().isEmpty
            ? widget.session.username
            : widget.session.fullName.trim();

    return Scaffold(
      appBar: AppBar(
        title: const Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.graphic_eq, color: AppColors.blueSoft),
            SizedBox(width: 8),
            Text('ConfiVoice'),
          ],
        ),
        actions: [
          IconButton(
            tooltip: 'Lihat analisis',
            onPressed: _showSavedAnalyses,
            icon: const Icon(Icons.history_outlined),
          ),
          IconButton(
            tooltip: 'Pengaturan API',
            onPressed: _showApiSettings,
            icon: const Icon(Icons.settings_outlined),
          ),
          Padding(
            padding: const EdgeInsets.only(right: 12),
            child: Center(
              child: ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 92),
                child: Text(
                  displayUserName,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    color: AppColors.text,
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
      body: SafeArea(
        child: _PredictionPage(
          studentNameController: _studentNameController,
          studentNames: _studentNames,
          studentGender: _studentGender,
          hasStoredStudentGender: _hasStoredGenderForCurrentStudent,
          selectedAudio: _selectedAudio,
          liveWavePreview: _liveWavePreview,
          wavePreview: _wavePreview,
          prediction: _prediction,
          errorMessage: _errorMessage,
          isPredicting: _isPredicting,
          isSaving: _isSaving,
          isRecording: _isRecording,
          onAddStudentName: _rememberStudentName,
          onSelectStudentName: _applyStudentGenderFromName,
          onGenderChanged: (value) {
            setState(() {
              _studentGender = value ?? '';
              _prediction = null;
              _errorMessage = null;
            });
          },
          onPickAudio: _pickAudio,
          onToggleRecording: _toggleRecording,
          onPredict: _predictAudio,
          onSave: _savePrediction,
        ),
      ),
    );
  }

  Future<void> _showApiSettings() async {
    await showDialog<void>(
      context: context,
      builder:
          (context) => AlertDialog(
            backgroundColor: AppColors.surface,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(14),
              side: const BorderSide(color: AppColors.border),
            ),
            title: const Text('Pengaturan'),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                TextField(
                  controller: _apiController,
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: 'URL prediksi',
                  ),
                  keyboardType: TextInputType.url,
                ),
                const SizedBox(height: 12),
                OutlinedButton(
                  onPressed: () {
                    Navigator.pop(context);
                    _confirmLogout();
                  },
                  child: const Text('Logout'),
                ),
              ],
            ),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('Batal'),
              ),
              FilledButton.icon(
                onPressed: () async {
                  await _saveApiEndpoint();
                  if (context.mounted) {
                    Navigator.pop(context);
                  }
                },
                icon: const Icon(Icons.save_outlined),
                label: const Text('Simpan'),
              ),
            ],
          ),
    );
  }
}

class _StudentData {
  const _StudentData({required this.names, required this.genders});

  final List<String> names;
  final Map<String, String> genders;
}

class _PredictionApiException implements Exception {
  const _PredictionApiException(this.statusCode, this.message);

  final int statusCode;
  final String message;

  @override
  String toString() => 'API error $statusCode: $message';
}

String _readApiErrorMessage(String responseBody) {
  try {
    final decoded = jsonDecode(responseBody) as Map<String, dynamic>;
    final detail = decoded['detail'];
    if (detail is String && detail.trim().isNotEmpty) {
      return detail.trim();
    }
    if (detail != null) {
      return detail.toString();
    }
  } catch (_) {}
  return responseBody.trim().isEmpty
      ? 'Server mengembalikan respons kosong.'
      : responseBody.trim();
}
