import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:llamadart/llamadart.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart';

import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../helpers/gif_helper.dart';
import '../utils/app_logger.dart';
import 'app_settings_service.dart';
import 'translation_file_store.dart';

class TranslationResult {
  final String translatedText;
  final String targetLanguageCode;
  final String? detectedLanguageCode;
  final String? modelId;
  final MessageTranslationStatus status;

  const TranslationResult({
    required this.translatedText,
    required this.targetLanguageCode,
    required this.status,
    this.detectedLanguageCode,
    this.modelId,
  });
}

class TranslationDownloadCancelled implements Exception {
  const TranslationDownloadCancelled();

  @override
  String toString() => 'Download canceled.';
}

class TranslationService extends ChangeNotifier {
  final AppSettingsService _appSettingsService;
  final TranslationFileStore _fileStore;

  TranslationService(
    this._appSettingsService, {
    TranslationFileStore? fileStore,
  }) : _fileStore = fileStore ?? TranslationFileStore() {
    // Initialize langdetect once at service construction.
    _langDetectInit = initLangDetect();
  }

  bool _disposed = false;
  bool _isBusy = false;
  bool _isDownloading = false;
  bool _cancelDownloadRequested = false;
  String? _lastError;
  Future<void> _queue = Future<void>.value();
  LlamaEngine? _engine;
  String? _loadedModelPath;
  String? _failedModelPath;
  Future<void>? _langDetectInit;
  int _downloadedBytes = 0;
  int? _downloadTotalBytes;
  String? _downloadFileName;

  bool get isBusy => _isBusy;
  bool get isDownloading => _isDownloading;
  String? get lastError => _lastError;
  int get downloadedBytes => _downloadedBytes;
  int? get downloadTotalBytes => _downloadTotalBytes;
  String? get downloadFileName => _downloadFileName;
  double? get downloadProgress {
    final total = _downloadTotalBytes;
    if (!_isDownloading || total == null || total <= 0) {
      return null;
    }
    return (_downloadedBytes / total).clamp(0.0, 1.0);
  }

  AppSettings get _settings => _appSettingsService.settings;

  String? resolvedTargetLanguageCode(String? fallbackLanguageCode) {
    return _settings.translationTargetLanguageCode ??
        _settings.languageOverride ??
        fallbackLanguageCode;
  }

  String? resolvedIncomingLanguageCode(String? fallbackLanguageCode) {
    return _settings.translationTargetLanguageCode ??
        _settings.languageOverride ??
        fallbackLanguageCode ??
        'en';
  }

  bool shouldAutoTranslateIncoming({
    required String text,
    required bool isCli,
    required bool isOutgoing,
  }) {
    if (!_settings.autoTranslateIncomingMessages) {
      return false;
    }
    return canTranslateIncoming(
      text: text,
      isCli: isCli,
      isOutgoing: isOutgoing,
    );
  }

  bool canTranslateIncoming({
    required String text,
    required bool isCli,
    required bool isOutgoing,
  }) {
    if (!_settings.translationEnabled || isCli || isOutgoing) {
      return false;
    }
    return _isPlainTextEligible(text);
  }

  bool shouldTranslateOutgoing({
    required String text,
    required String? targetLanguageCode,
  }) {
    return _settings.composerTranslationEnabled &&
        targetLanguageCode != null &&
        targetLanguageCode.isNotEmpty &&
        _isPlainTextEligible(text);
  }

  List<TranslationModelRecord> get availableModels =>
      _settings.translationDownloadedModels;

  TranslationModelRecord? get selectedModel {
    final selectedId = _settings.translationSelectedModelId;
    if (selectedId == null) {
      return availableModels.isNotEmpty ? availableModels.first : null;
    }
    for (final model in availableModels) {
      if (model.id == selectedId) {
        return model;
      }
    }
    return availableModels.isNotEmpty ? availableModels.first : null;
  }

  Future<void> refreshDownloadedModels() async {
    if (_isDownloading) return;
    final scanned = await _fileStore.scanDownloadedModels();
    if (scanned.isEmpty) {
      return;
    }
    final existingByPath = {
      for (final model in _settings.translationDownloadedModels)
        model.localPath: model,
    };
    final merged = scanned.map((model) {
      final existing = existingByPath[model.localPath];
      if (existing == null) {
        return model;
      }
      return TranslationModelRecord(
        id: existing.id,
        name: existing.name,
        sourceUrl: existing.sourceUrl,
        localPath: existing.localPath,
        downloadedAt: existing.downloadedAt,
        fileSizeBytes: model.fileSizeBytes,
      );
    }).toList();
    await _appSettingsService.setTranslationDownloadedModels(merged);
    _failedModelPath = null;
    if (_settings.translationSelectedModelId == null && merged.isNotEmpty) {
      await _appSettingsService.setTranslationSelectedModelId(merged.first.id);
    }
  }

  static const int _parallelChunks = 8;
  static const int _parallelMinBytes = 10 * 1024 * 1024; // 10 MB

  Future<TranslationModelRecord> downloadModel({
    required String sourceUrl,
    String? fileName,
    String? id,
  }) async {
    final uri = Uri.tryParse(sourceUrl);
    if (uri == null || !uri.hasScheme) {
      throw ArgumentError('Invalid model URL.');
    }
    return _runExclusive(() async {
      _setBusy(true);
      _setDownloading(true);
      _lastError = null;
      try {
        final resolvedFileName =
            fileName ??
            _sanitizeFileName(
              uri.pathSegments.isNotEmpty
                  ? uri.pathSegments.last
                  : 'translation-model.gguf',
            );
        _downloadFileName = resolvedFileName;
        _downloadedBytes = 0;
        _cancelDownloadRequested = false;

        // HEAD request to check size and range support.
        final headClient = http.Client();
        int? totalSize;
        bool supportsRange = false;
        try {
          final headResponse = await headClient.send(http.Request('HEAD', uri));
          totalSize = headResponse.contentLength;
          supportsRange =
              headResponse.headers['accept-ranges']?.contains('bytes') == true;
          await headResponse.stream.drain<void>();
        } finally {
          headClient.close();
        }

        _downloadTotalBytes = totalSize;
        _notify();

        DownloadedModelFile downloaded;
        if (supportsRange &&
            totalSize != null &&
            totalSize > _parallelMinBytes) {
          downloaded = await _downloadParallel(
            uri: uri,
            fileName: resolvedFileName,
            totalSize: totalSize,
          );
        } else {
          downloaded = await _downloadSingle(
            uri: uri,
            fileName: resolvedFileName,
          );
        }

        final record = TranslationModelRecord(
          id: id ?? resolvedFileName,
          name: resolvedFileName,
          sourceUrl: sourceUrl,
          localPath: downloaded.localPath,
          downloadedAt: DateTime.now(),
          fileSizeBytes: downloaded.fileSizeBytes,
        );
        final updated = [
          for (final existing in _settings.translationDownloadedModels)
            if (existing.id != record.id) existing,
          record,
        ];
        await _appSettingsService.setTranslationDownloadedModels(updated);
        await _appSettingsService.setTranslationSelectedModelId(record.id);
        await _appSettingsService.setTranslationModelSourceUrl(sourceUrl);
        _failedModelPath = null;
        return record;
      } finally {
        _setDownloading(false);
      }
    });
  }

  Future<DownloadedModelFile> _downloadSingle({
    required Uri uri,
    required String fileName,
  }) async {
    final client = http.Client();
    try {
      final response = await client.send(http.Request('GET', uri));
      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw StateError('Model download failed: HTTP ${response.statusCode}');
      }
      _downloadTotalBytes ??= response.contentLength;
      _notify();
      final trackedStream = _trackDownloadProgress(response.stream);
      return await _fileStore.writeModelBytes(
        fileName: fileName,
        chunks: trackedStream,
      );
    } finally {
      client.close();
    }
  }

  Future<DownloadedModelFile> _downloadParallel({
    required Uri uri,
    required String fileName,
    required int totalSize,
  }) async {
    final chunkSize = (totalSize / _parallelChunks).ceil();
    final chunkPaths = <String>[];
    final clients = <http.Client>[];
    var combineReached = false;
    try {
      final futures = <Future<void>>[];
      for (var i = 0; i < _parallelChunks; i++) {
        final start = i * chunkSize;
        final end = (start + chunkSize - 1).clamp(0, totalSize - 1);
        if (start >= totalSize) break;
        final chunkPath = await _fileStore.chunkFilePath(fileName, i);
        chunkPaths.add(chunkPath);
        final client = http.Client();
        clients.add(client);
        futures.add(
          _downloadRange(
            client: client,
            uri: uri,
            chunkPath: chunkPath,
            start: start,
            end: end,
          ),
        );
      }
      await Future.wait(futures);
      if (_cancelDownloadRequested) {
        throw const TranslationDownloadCancelled();
      }
      _downloadFileName = 'Merging chunks...';
      _notify();
      combineReached = true;
      return await _fileStore.combineChunks(
        fileName: fileName,
        chunkPaths: chunkPaths,
      );
    } finally {
      for (final client in clients) {
        client.close();
      }
      if (!combineReached) {
        for (final chunkPath in chunkPaths) {
          await _fileStore.deleteFile(chunkPath);
        }
      }
    }
  }

  Future<void> _downloadRange({
    required http.Client client,
    required Uri uri,
    required String chunkPath,
    required int start,
    required int end,
  }) async {
    final request = http.Request('GET', uri);
    request.headers['Range'] = 'bytes=$start-$end';
    final response = await client.send(request);
    if (response.statusCode != 206) {
      await response.stream.drain<void>();
      throw StateError(
        'Range download failed: HTTP ${response.statusCode}'
        '${response.statusCode == 200 ? ' (server ignored Range header)' : ''}',
      );
    }
    final trackedStream = _trackDownloadProgress(response.stream);
    await _fileStore.writeModelBytes(
      fileName: chunkPath.split(RegExp(r'[/\\]')).last,
      chunks: trackedStream,
    );
  }

  void cancelDownload() {
    if (!_isDownloading) {
      return;
    }
    _cancelDownloadRequested = true;
    _lastError = 'Download stopped.';
    _notify();
  }

  Future<void> removeModel(TranslationModelRecord model) async {
    await _runExclusive(() async {
      _setBusy(true);
      _lastError = null;
      await _fileStore.deleteModel(model);
      final updated = _settings.translationDownloadedModels
          .where((entry) => entry.id != model.id)
          .toList();
      await _appSettingsService.setTranslationDownloadedModels(updated);
      if (_settings.translationSelectedModelId == model.id) {
        await _appSettingsService.setTranslationSelectedModelId(
          updated.isNotEmpty ? updated.first.id : null,
        );
      }
    });
  }

  Future<TranslationResult?> translateIncomingText({
    required String text,
    required String? targetLanguageCode,
  }) async {
    if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
      return null;
    }
    final detectedLanguageCode = await detectLanguage(
      _stripReplyInfoForDetection(text),
    );
    if (detectedLanguageCode != null &&
        detectedLanguageCode == targetLanguageCode) {
      return const TranslationResult(
        translatedText: '',
        targetLanguageCode: '',
        status: MessageTranslationStatus.skipped,
      );
    }
    final translatedText = await _translateText(
      text: text,
      targetLanguageCode: targetLanguageCode,
      sourceLanguageCode: detectedLanguageCode,
    );
    if (translatedText == null || translatedText.trim().isEmpty) {
      return null;
    }
    // If translation is nearly identical, text was already in target language.
    if (translatedText.trim().toLowerCase() == text.trim().toLowerCase()) {
      return const TranslationResult(
        translatedText: '',
        targetLanguageCode: '',
        status: MessageTranslationStatus.skipped,
      );
    }
    return TranslationResult(
      translatedText: translatedText.trim(),
      targetLanguageCode: targetLanguageCode,
      detectedLanguageCode: detectedLanguageCode,
      modelId: selectedModel?.id,
      status: MessageTranslationStatus.completed,
    );
  }

  Future<TranslationResult?> translateOutgoingText({
    required String text,
    required String? targetLanguageCode,
  }) async {
    if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
      return null;
    }
    final detectedLanguageCode = await detectLanguage(
      _stripReplyInfoForDetection(text),
    );
    if (detectedLanguageCode != null &&
        detectedLanguageCode == targetLanguageCode) {
      return const TranslationResult(
        translatedText: '',
        targetLanguageCode: '',
        status: MessageTranslationStatus.skipped,
      );
    }
    final translatedText = await _translateText(
      text: text,
      targetLanguageCode: targetLanguageCode,
      sourceLanguageCode: detectedLanguageCode,
    );
    if (translatedText == null || translatedText.trim().isEmpty) {
      return null;
    }
    return TranslationResult(
      translatedText: translatedText.trim(),
      targetLanguageCode: targetLanguageCode,
      detectedLanguageCode: detectedLanguageCode,
      modelId: selectedModel?.id,
      status: MessageTranslationStatus.completed,
    );
  }

  Future<String?> detectLanguage(String text) async {
    try {
      // Ensure the detector is initialized (constructor starts init).
      await (_langDetectInit ??= initLangDetect());
      final code = detect(text);
      if (code.isEmpty) return null;
      return code;
    } catch (error) {
      _lastError = error.toString();
      appLogger.warn('Language detection failed: $error');
      _notify();
      return null;
    }
  }

  String _stripReplyInfoForDetection(String text) {
    final match = RegExp(
      r'@\[([^\]]+)\]\s+(.+)$',
      dotAll: true,
    ).firstMatch(text);
    return match?.group(2) ?? text;
  }

  Future<String?> _translateText({
    required String text,
    required String targetLanguageCode,
    String? sourceLanguageCode,
  }) async {
    if (!_hasUsableModel) {
      return null;
    }
    final model = selectedModel;
    if (model == null || model.localPath.isEmpty) {
      return null;
    }
    final targetLabel = _languageLabel(targetLanguageCode);
    final instruction = targetLanguageCode == 'zh'
        ? '将以下文本翻译为中文，注意只需要输出翻译后的结果，不要额外解释：\n\n$text'
        : 'Translate the following segment into $targetLabel, without additional explanation.\n\n$text';
    try {
      return await _runExclusive(() async {
        final engine = await _ensureContext(model.localPath);
        if (engine == null) {
          return null;
        }
        final messages = [
          LlamaChatMessage.fromText(
            role: LlamaChatRole.user,
            text: instruction,
          ),
        ];
        final output = StringBuffer();
        await for (final chunk in engine.create(
          messages,
          params: const GenerationParams(
            maxTokens: 256,
            temp: 0.7,
            topK: 20,
            topP: 0.6,
            penalty: 1.05,
            reusePromptPrefix: false,
          ),
          enableThinking: false,
          sourceLangCode: sourceLanguageCode,
          targetLangCode: targetLanguageCode,
        )) {
          final content = chunk.choices.firstOrNull?.delta.content;
          if (content != null) {
            output.write(content);
          }
          if (output.length >= text.length * 4 + 100) {
            break;
          }
        }
        return _sanitizeOutput(output.toString());
      });
    } catch (error) {
      _lastError = error.toString();
      appLogger.warn('Translation request failed: $error');
      _notify();
      return null;
    }
  }

  bool get _hasUsableModel {
    final model = selectedModel;
    return !kIsWeb && model != null && model.localPath.isNotEmpty;
  }

  bool _isPlainTextEligible(String text) {
    final trimmed = text.trim();
    if (trimmed.isEmpty) {
      return false;
    }
    if (GifHelper.parseGif(trimmed) != null) {
      return false;
    }
    return !(trimmed.startsWith('m:') ||
        trimmed.startsWith('V1|') ||
        trimmed.startsWith('r:'));
  }

  String _languageLabel(String code) {
    for (final option in supportedTranslationLanguages) {
      if (option.code == code) {
        return option.label;
      }
    }
    return code.toUpperCase();
  }

  String _sanitizeOutput(String raw) {
    var result = raw.trim();
    result = result.replaceAll(RegExp(r'\*\*'), '');
    result = result.replaceAll(RegExp(r'<[^>]+>'), '');
    return result.trim();
  }

  String _sanitizeFileName(String fileName) {
    final cleaned = fileName.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
    return cleaned.isEmpty ? 'translation-model.gguf' : cleaned;
  }

  Future<LlamaEngine?> _ensureContext(String modelPath) async {
    if (_engine != null && _loadedModelPath == modelPath) {
      return _engine;
    }
    if (modelPath == _failedModelPath) {
      return null;
    }
    if (_engine != null) {
      await _engine!.dispose();
      _engine = null;
      _loadedModelPath = null;
    }
    final engine = LlamaEngine(LlamaBackend());
    try {
      await engine.loadModel(
        modelPath,
        modelParams: const ModelParams(
          gpuLayers: 0,
          preferredBackend: GpuBackend.cpu,
        ),
      );
      _engine = engine;
      _loadedModelPath = modelPath;
      _failedModelPath = null;
      return _engine;
    } catch (_) {
      await engine.dispose();
      _failedModelPath = modelPath;
      rethrow;
    }
  }

  Future<void> releaseModel() async {
    await _runExclusive(() async {
      final engine = _engine;
      if (engine == null) {
        _loadedModelPath = null;
        return;
      }
      _engine = null;
      _loadedModelPath = null;
      await engine.dispose();
    });
  }

  Future<T> _runExclusive<T>(Future<T> Function() action) {
    final completer = Completer<T>();
    _setBusy(true);
    _queue = _queue.then((_) async {
      if (_disposed) {
        completer.completeError(StateError('TranslationService disposed.'));
        return;
      }
      try {
        completer.complete(await action());
      } catch (error, stackTrace) {
        completer.completeError(error, stackTrace);
      } finally {
        _setBusy(false);
      }
    });
    return completer.future;
  }

  Stream<List<int>> _trackDownloadProgress(Stream<List<int>> source) async* {
    await for (final chunk in source) {
      if (_cancelDownloadRequested) {
        throw const TranslationDownloadCancelled();
      }
      _downloadedBytes += chunk.length;
      _notify();
      yield chunk;
    }
  }

  void _notify() {
    if (_disposed) {
      return;
    }
    notifyListeners();
  }

  void _setBusy(bool value) {
    if (_isBusy == value) {
      return;
    }
    _isBusy = value;
    _notify();
  }

  void _setDownloading(bool value) {
    _isDownloading = value;
    if (!value) {
      _cancelDownloadRequested = false;
      _downloadedBytes = 0;
      _downloadTotalBytes = null;
      _downloadFileName = null;
    }
    _notify();
  }

  @override
  void dispose() {
    _disposed = true;
    final engine = _engine;
    _engine = null;
    _loadedModelPath = null;
    if (engine != null) {
      unawaited(engine.dispose());
    }
    super.dispose();
  }
}
