import 'dart:convert';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:http/http.dart' as http;

/// Unified AI text-enhancement service.
///
/// Tries Gemini (free-tier flash/lite models) first, with automatic
/// 429-retry across all discovered models.  If every Gemini model fails
/// (quota exhausted or any unrecoverable error) it seamlessly falls back
/// to the DeepSeek API.
///
/// Usage:
/// ```dart
/// final result = await AiService().enhanceText(
///   myText,
///   promptInstruction: 'Fix grammar and translate to English …',
/// );
/// ```
class AiService {
  static final AiService _instance = AiService._internal();
  factory AiService() => _instance;

  late final String _geminiApiKey;
  late final String _deepseekApiKey;

  /// Cached Gemini model IDs (flash / lite, generateContent-capable).
  List<String> _geminiModels = [];

  AiService._internal() {
    final gKey = dotenv.env['GEMINI_API_KEY'];
    if (gKey == null || gKey.isEmpty) {
      throw Exception('GEMINI_API_KEY not found in .env');
    }
    _geminiApiKey = gKey;

    final dsKey = dotenv.env['DEEPSEEK_API_KEY'];
    if (dsKey == null || dsKey.isEmpty) {
      throw Exception('DEEPSEEK_API_KEY not found in .env');
    }
    _deepseekApiKey = dsKey;
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  /// Fixes spelling / grammar, improves clarity, and translates [text] to
  /// professional English.
  ///
  /// Supply [promptInstruction] to give the model field-specific context
  /// (e.g. "This is an IT helpdesk ticket subject …").  If omitted a
  /// sensible generic instruction is used.
  ///
  /// Order of preference:
  ///   1. Gemini flash / lite models (auto-retry on 429 across all models)
  ///   2. DeepSeek `deepseek-chat` (fallback on total Gemini failure)
  ///
  /// Throws only if **both** providers fail.
  /// [onFallbackToDeepSeek] is called (from the same isolate) just before
  /// switching to the DeepSeek provider, so callers can update UI accordingly.
  ///
  /// This method never throws — if both providers fail it returns [text] unchanged.
  Future<String> enhanceText(
    String text, {
    String? promptInstruction,
    void Function()? onFallbackToDeepSeek,
  }) async {
    if (text.trim().isEmpty) return text;

    final instruction =
        promptInstruction ??
        'Fix spelling and grammar, improve clarity, and translate to '
            'professional English. Return ONLY the improved text, '
            'no explanations:';
    final prompt = '$instruction\n\n"$text"';

    // --- 1. Try Gemini ---
    try {
      return await _geminiGenerate(prompt, fallback: text);
    } catch (_) {
      // All Gemini models failed — fall through to DeepSeek.
      onFallbackToDeepSeek?.call();
    }

    // --- 2. Fallback: DeepSeek ---
    try {
      return await _deepseekGenerate(prompt, fallback: text);
    } catch (_) {
      // Both providers failed — return original text unchanged.
      return text;
    }
  }

  // ---------------------------------------------------------------------------
  // Gemini
  // ---------------------------------------------------------------------------

  Future<List<String>> _getGeminiModels() async {
    if (_geminiModels.isNotEmpty) return _geminiModels;

    try {
      final uri = Uri.parse(
        'https://generativelanguage.googleapis.com/v1beta/models'
        '?key=$_geminiApiKey',
      );
      final res = await http.get(uri);
      if (res.statusCode == 200) {
        final data = jsonDecode(res.body) as Map<String, dynamic>;
        final rawModels = (data['models'] as List<dynamic>?) ?? [];
        final discovered = <String>[];
        for (final m in rawModels) {
          final fullName = m['name'] as String? ?? '';
          final lower = fullName.toLowerCase();
          final methods =
              (m['supportedGenerationMethods'] as List<dynamic>?) ?? [];
          if (methods.contains('generateContent') &&
              (lower.contains('flash') || lower.contains('lite'))) {
            final id = fullName.startsWith('models/')
                ? fullName.substring('models/'.length)
                : fullName;
            discovered.add(id);
          }
        }
        discovered.sort((a, b) => b.compareTo(a));
        _geminiModels = discovered;
      }
    } catch (_) {
      // Fall through to hard-coded list.
    }

    if (_geminiModels.isEmpty) {
      _geminiModels = [
        'gemini-2.5-flash-lite',
        'gemini-2.5-flash',
        'gemini-2.0-flash',
        'gemini-1.5-flash',
      ];
    }

    return _geminiModels;
  }

  Future<String> _geminiGenerate(
    String prompt, {
    required String fallback,
  }) async {
    final models = await _getGeminiModels();

    Object? lastError;
    for (final modelId in models) {
      try {
        final model = GenerativeModel(model: modelId, apiKey: _geminiApiKey);
        final response = await model.generateContent([Content.text(prompt)]);
        return response.text ?? fallback;
      } catch (e) {
        lastError = e;
        // Try the next model regardless of error type.
      }
    }

    throw Exception('All Gemini models failed. Last error: $lastError');
  }

  // ---------------------------------------------------------------------------
  // DeepSeek (OpenAI-compatible REST)
  // ---------------------------------------------------------------------------

  Future<String> _deepseekGenerate(
    String prompt, {
    required String fallback,
  }) async {
    const url = 'https://api.deepseek.com/chat/completions';
    final body = jsonEncode({
      'model': 'deepseek-chat',
      'messages': [
        {'role': 'user', 'content': prompt},
      ],
    });

    final res = await http.post(
      Uri.parse(url),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $_deepseekApiKey',
      },
      body: body,
    );

    if (res.statusCode == 200) {
      final data = jsonDecode(res.body) as Map<String, dynamic>;
      final choices = data['choices'] as List<dynamic>?;
      final content = choices?.firstOrNull?['message']?['content'] as String?;
      return content?.trim() ?? fallback;
    }

    throw Exception(
      'DeepSeek request failed (HTTP ${res.statusCode}): ${res.body}',
    );
  }
}
