import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
import '../models/am_registration_data.dart';
import '../models/user_registration_data.dart';
import '../models/reprise_dossier.dart';
import '../utils/parent_registration_payload.dart';
import 'api/api_config.dart';
import 'api/tokenService.dart';
import '../utils/nir_utils.dart';
import '../utils/email_utils.dart';

class AuthService {
  static const String _currentUserKey = 'current_user';

  static String? _basenameFromPath(String path) {
    final s = path.replaceAll(r'\', '/');
    final i = s.lastIndexOf('/');
    final name = i >= 0 ? s.substring(i + 1) : s;
    return name.isEmpty ? null : name;
  }

  static String _imageMimeForBytes(Uint8List bytes) {
    if (bytes.length >= 8 &&
        bytes[0] == 0x89 &&
        bytes[1] == 0x50 &&
        bytes[2] == 0x4E &&
        bytes[3] == 0x47) {
      return 'image/png';
    }
    return 'image/jpeg';
  }

  /// Connexion de l'utilisateur
  /// Retourne l'utilisateur connecté avec le flag changement_mdp_obligatoire
  static Future<AppUser> login(String email, String password) async {
    try {
      final response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.login}'),
        headers: ApiConfig.headers,
        body: jsonEncode({
          'email': email,
          'password': password,
        }),
      );

      if (response.statusCode == 200 || response.statusCode == 201) {
        final data = jsonDecode(response.body);
        // API renvoie access_token / refresh_token (snake_case)
        final accessToken = data['access_token'] as String? ?? data['accessToken'] as String?;
        final refreshToken = data['refresh_token'] as String? ?? data['refreshToken'] as String?;
        if (accessToken == null) throw Exception('Token absent dans la réponse serveur');

        await TokenService.saveToken(accessToken);
        await TokenService.saveRefreshToken(refreshToken ?? '');

        final user = await _fetchUserProfile(accessToken);
        
        // Stocker l'utilisateur en cache
        await _saveCurrentUser(user);
        
        return user;
      } else {
        final error = jsonDecode(response.body);
        throw Exception(error['message'] ?? 'Erreur de connexion');
      }
    } catch (e) {
      if (e is Exception) rethrow;
      // Erreurs non-Exception (ex. certains cas côté web) : afficher le détail pour le diagnostic
      throw Exception(
        'Erreur réseau: impossible de se connecter au serveur (${e.runtimeType}: $e)',
      );
    }
  }

  /// Récupère le profil utilisateur via /auth/me
  static Future<AppUser> _fetchUserProfile(String token) async {
    try {
      final response = await http.get(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.authMe}'),
        headers: ApiConfig.authHeaders(token),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return AppUser.fromJson(data);
      } else {
        throw Exception('Erreur lors de la récupération du profil');
      }
    } catch (e) {
      if (e is Exception) rethrow;
      throw Exception('Erreur réseau: impossible de récupérer le profil');
    }
  }

  /// Changement de mot de passe obligatoire
  static Future<void> changePasswordRequired({
    required String currentPassword,
    required String newPassword,
  }) async {
    final token = await TokenService.getToken();
    if (token == null) {
      throw Exception('Non authentifié');
    }

    try {
      final response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.changePasswordRequired}'),
        headers: ApiConfig.authHeaders(token),
        body: jsonEncode({
          'mot_de_passe_actuel': currentPassword,
          'nouveau_mot_de_passe': newPassword,
          'confirmation_mot_de_passe': newPassword,
        }),
      );

      if (response.statusCode != 200 && response.statusCode != 201) {
        final error = jsonDecode(response.body);
        throw Exception(error['message'] ?? 'Erreur lors du changement de mot de passe');
      }

      // Après le changement de MDP, rafraîchir le profil utilisateur
      final user = await _fetchUserProfile(token);
      await _saveCurrentUser(user);
    } catch (e) {
      if (e is Exception) rethrow;
      throw Exception('Erreur réseau: impossible de changer le mot de passe');
    }
  }

  /// Vérifie qu'un token de création de mot de passe est encore valide.
  /// Retourne `true` si l'API renvoie 200 ; `false` si 404.
  static Future<bool> verifyCreatePasswordToken(String token) async {
    final cleaned = token.trim();
    if (cleaned.isEmpty) return false;
    try {
      final uri = Uri.parse(
        '${ApiConfig.baseUrl}${ApiConfig.verifyCreatePasswordToken}?token=${Uri.encodeQueryComponent(cleaned)}',
      );
      final response = await http.get(uri, headers: ApiConfig.headers);
      if (response.statusCode == 200) return true;
      if (response.statusCode == 404) return false;
      final decoded = _tryDecodeJsonMap(response.body);
      throw Exception(_extractErrorMessage(decoded, response.statusCode));
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }
  }

  /// Crée le mot de passe initial via token email.
  static Future<void> createPasswordWithToken({
    required String token,
    required String password,
    required String passwordConfirmation,
  }) async {
    final cleaned = token.trim();
    if (cleaned.isEmpty) {
      throw Exception('Lien invalide ou expiré.');
    }
    late final http.Response response;
    try {
      response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.createPassword}'),
        headers: ApiConfig.headers,
        body: jsonEncode({
          'token': cleaned,
          'password': password,
          'password_confirmation': passwordConfirmation,
        }),
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }

    if (response.statusCode == 200 || response.statusCode == 201) {
      return;
    }
    final decoded = _tryDecodeJsonMap(response.body);
    throw Exception(_extractErrorMessage(decoded, response.statusCode));
  }

  /// Demande de réinitialisation du mot de passe (ticket #127).
  /// Réponse **toujours neutre** côté UX si le serveur répond (anti-énumération des comptes).
  static Future<void> requestForgotPassword(String email) async {
    final cleaned = normalizeEmailText(email);
    if (cleaned.isEmpty) {
      throw Exception('Veuillez saisir votre adresse e-mail.');
    }
    late final http.Response response;
    try {
      response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.forgotPassword}'),
        headers: ApiConfig.headers,
        body: jsonEncode({'email': cleaned}),
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }
    if (response.statusCode >= 500) {
      final decoded = _tryDecodeJsonMap(response.body);
      throw Exception(_extractErrorMessage(decoded, response.statusCode));
    }
  }

  /// Réinitialise le mot de passe via le lien e-mail (ticket #127).
  static Future<void> resetPasswordWithToken({
    required String token,
    required String password,
    required String passwordConfirmation,
  }) async {
    final cleaned = token.trim();
    if (cleaned.isEmpty) {
      throw Exception('Lien invalide ou expiré.');
    }
    late final http.Response response;
    try {
      response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.resetPassword}'),
        headers: ApiConfig.headers,
        body: jsonEncode({
          'token': cleaned,
          'password': password,
          'password_confirmation': passwordConfirmation,
        }),
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }

    if (response.statusCode == 200 || response.statusCode == 201) {
      return;
    }
    if (response.statusCode == 404) {
      throw Exception('Lien invalide ou expiré.');
    }
    final decoded = _tryDecodeJsonMap(response.body);
    final msg = _extractErrorMessage(decoded, response.statusCode);
    final lower = msg.toLowerCase();
    if (response.statusCode == 400 &&
        (lower.contains('token') ||
            lower.contains('invalide') ||
            lower.contains('expiré') ||
            lower.contains('expire'))) {
      throw Exception('Lien invalide ou expiré.');
    }
    throw Exception(msg);
  }

  /// Charge le dossier pour reprise (lien e-mail). GET /auth/reprise-dossier. Ticket #112.
  static Future<RepriseDossier> getRepriseDossier(String token) async {
    final cleaned = token.trim();
    if (cleaned.isEmpty) {
      throw Exception('Lien invalide ou expiré.');
    }
    final uri = Uri.parse(
      '${ApiConfig.baseUrl}${ApiConfig.repriseDossier}',
    ).replace(queryParameters: {'token': cleaned});
    late final http.Response response;
    try {
      response = await http.get(uri, headers: ApiConfig.headers);
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }
    if (response.statusCode == 404) {
      throw Exception('Lien invalide ou expiré.');
    }
    if (response.statusCode != 200) {
      final decoded = _tryDecodeJsonMap(response.body);
      throw Exception(_extractErrorMessage(decoded, response.statusCode));
    }
    final decoded = jsonDecode(response.body);
    if (decoded is! Map<String, dynamic>) {
      throw Exception('Réponse invalide du serveur.');
    }
    final payload = decoded['data'] is Map
        ? Map<String, dynamic>.from(decoded['data'] as Map)
        : decoded;
    return RepriseDossier.fromJson(payload);
  }

  /// Modale login : numéro + e-mail → token reprise. POST /auth/reprise-identify. #112.
  static Future<String> identifyReprise({
    required String numeroDossier,
    required String email,
  }) async {
    final num = numeroDossier.trim();
    final mail = normalizeEmailText(email);
    if (num.isEmpty || mail.isEmpty) {
      throw Exception('Numéro de dossier et e-mail requis.');
    }
    late final http.Response response;
    try {
      response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.repriseIdentify}'),
        headers: ApiConfig.headers,
        body: jsonEncode({
          'numero_dossier': num,
          'email': mail,
        }),
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }
    if (response.statusCode == 404) {
      throw Exception(
        'Aucun dossier en reprise trouvé pour ce numéro et cet e-mail.',
      );
    }
    if (response.statusCode != 200 && response.statusCode != 201) {
      final decoded = _tryDecodeJsonMap(response.body);
      throw Exception(_extractErrorMessage(decoded, response.statusCode));
    }
    final decoded = jsonDecode(response.body);
    if (decoded is! Map<String, dynamic>) {
      throw Exception('Réponse invalide du serveur.');
    }
    final token = decoded['token']?.toString().trim() ?? '';
    if (token.isEmpty) {
      throw Exception('Réponse invalide du serveur.');
    }
    return token;
  }

  /// Resoumission après refus. PATCH /auth/reprise-resoumettre. Ticket #112.
  static Future<void> resoumettreReprise(Map<String, dynamic> body) async {
    final cleaned = body['token']?.toString().trim() ?? '';
    if (cleaned.isEmpty) {
      throw Exception('Lien invalide ou expiré.');
    }
    final payload = Map<String, dynamic>.from(body);
    payload['token'] = cleaned;

    late final http.Response response;
    try {
      response = await http.patch(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.repriseResoumettre}'),
        headers: ApiConfig.headers,
        body: jsonEncode(payload),
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
      );
    }
    if (response.statusCode == 404) {
      throw Exception('Lien invalide ou expiré.');
    }
    if (response.statusCode == 200 || response.statusCode == 201) {
      return;
    }
    final decoded = _tryDecodeJsonMap(response.body);
    throw Exception(_extractErrorMessage(decoded, response.statusCode));
  }

  /// Déconnexion de l'utilisateur
  static Future<void> logout() async {
    await TokenService.clearAll();
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_currentUserKey);
  }

  /// Vérifie si l'utilisateur est connecté
  static Future<bool> isLoggedIn() async {
    final token = await TokenService.getToken();
    return token != null;
  }

  /// Récupère l'utilisateur connecté depuis le cache
  static Future<AppUser?> getCurrentUser() async {
    final prefs = await SharedPreferences.getInstance();
    final userJson = prefs.getString(_currentUserKey);
    
    if (userJson != null) {
      return AppUser.fromJson(jsonDecode(userJson));
    }
    
    return null;
  }

  /// Sauvegarde l'utilisateur actuel en cache
  static Future<void> _saveCurrentUser(AppUser user) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_currentUserKey, jsonEncode(user.toJson()));
  }

  /// Inscription AM complète (POST /auth/register/am).
  /// En cas de succès (201), aucune donnée utilisateur retournée ; rediriger vers login.
  static Future<void> registerAM(AmRegistrationData data) async {
    if (data.agreementDate == null) {
      throw Exception('La date d\'obtention de l\'agrément est requise.');
    }
    if (data.placesAvailable == null) {
      throw Exception('Le nombre de places disponibles est requis.');
    }
    final lieuVille = data.birthCity.trim();
    final lieuPays = data.birthCountry.trim();
    if (lieuVille.length < 2 || lieuPays.length < 2) {
      throw Exception(
        'La ville et le pays de naissance sont requis (au moins 2 caractères chacun).',
      );
    }
    String? photoBase64;
    String? photoFilename;

    if (data.photoBytes != null && data.photoBytes!.isNotEmpty) {
      final mime = _imageMimeForBytes(data.photoBytes!);
      photoBase64 = 'data:$mime;base64,${base64Encode(data.photoBytes!)}';
      final fn = (data.photoFilename ?? '').trim();
      photoFilename = fn.isNotEmpty ? fn : 'photo_am.jpg';
    } else if (data.photoPath != null &&
        data.photoPath!.isNotEmpty &&
        !data.photoPath!.startsWith('assets/')) {
      if (!kIsWeb) {
        try {
          final file = File(data.photoPath!);
          if (await file.exists()) {
            final bytes = await file.readAsBytes();
            final mime = _imageMimeForBytes(bytes);
            photoBase64 = 'data:$mime;base64,${base64Encode(bytes)}';
            photoFilename =
                _basenameFromPath(data.photoPath!) ?? 'photo_am.jpg';
          }
        } catch (_) {}
      }
    }

    if (photoBase64 == null || photoBase64.isEmpty) {
      throw Exception('Une photo de profil est requise pour finaliser l’inscription.');
    }

    final body = <String, dynamic>{
      'email': data.email,
      'prenom': data.firstName,
      'nom': data.lastName,
      'telephone': data.phone,
      'adresse': data.streetAddress.isNotEmpty ? data.streetAddress : null,
      'code_postal': data.postalCode.isNotEmpty ? data.postalCode : null,
      'ville': data.city.isNotEmpty ? data.city : null,
      'photo_base64': photoBase64,
      'photo_filename': photoFilename ?? 'photo_am.jpg',
      'consentement_photo': data.photoConsent,
      'date_naissance': data.dateOfBirth != null
          ? '${data.dateOfBirth!.year}-${data.dateOfBirth!.month.toString().padLeft(2, '0')}-${data.dateOfBirth!.day.toString().padLeft(2, '0')}'
          : null,
      'lieu_naissance_ville': lieuVille,
      'lieu_naissance_pays': lieuPays,
      'nir': normalizeNir(data.nir),
      'numero_agrement': data.agrementNumber,
      'date_agrement':
          '${data.agreementDate!.year}-${data.agreementDate!.month.toString().padLeft(2, '0')}-${data.agreementDate!.day.toString().padLeft(2, '0')}',
      'capacite_accueil': data.capacity ?? 1,
      'places_disponibles': data.placesAvailable!,
      'biographie': data.presentationText.isNotEmpty ? data.presentationText : null,
      'acceptation_cgu': data.cguAccepted,
      'acceptation_privacy': data.cguAccepted,
    };

    final String encodedBody;
    try {
      encodedBody = jsonEncode(body);
    } catch (_) {
      throw Exception(
        'Impossible de préparer l’envoi (données trop volumineuses ou invalides). '
        'Réessayez avec une photo plus légère.',
      );
    }

    late final http.Response response;
    try {
      response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerAM}'),
        headers: ApiConfig.headers,
        body: encodedBody,
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau, '
        'un pare-feu ou un bloqueur, puis réessayez.',
      );
    }

    if (response.statusCode == 200 || response.statusCode == 201) {
      return;
    }

    final decoded = _tryDecodeJsonMap(response.body);
    final message = _extractErrorMessage(decoded, response.statusCode);
    throw Exception(message);
  }

  /// Inscription parent complète (POST /auth/register/parent).
  /// Succès : 201, pas de session — rediriger vers le login.
  static Future<void> registerParent(UserRegistrationData data) async {
    final validationError = ParentRegistrationPayload.validateForApi(data);
    if (validationError != null) {
      throw Exception(validationError);
    }

    final body = ParentRegistrationPayload.toJson(data);
    final String encodedBody;
    try {
      encodedBody = jsonEncode(body);
    } catch (_) {
      throw Exception(
        'Impossible de préparer l’envoi (données trop volumineuses ou invalides). '
        'Réessayez avec une photo plus légère ou sans caractères inhabituels.',
      );
    }

    late final http.Response response;
    try {
      response = await http.post(
        Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerParent}'),
        headers: ApiConfig.headers,
        body: encodedBody,
      );
    } on http.ClientException {
      throw Exception(
        'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau, '
        'un pare-feu ou un bloqueur, puis réessayez.',
      );
    }

    if (response.statusCode == 200 || response.statusCode == 201) {
      return;
    }

    final decoded = _tryDecodeJsonMap(response.body);
    var message = _extractErrorMessage(decoded, response.statusCode);
    message = _humanizeParentRegistrationHttpError(message, response.statusCode);
    throw Exception(message);
  }

  static Map<String, dynamic>? _tryDecodeJsonMap(String body) {
    if (body.isEmpty) return null;
    try {
      final decoded = jsonDecode(body);
      if (decoded is Map) {
        return Map<String, dynamic>.from(decoded);
      }
      return null;
    } catch (_) {
      return null;
    }
  }

  /// Aplatit `message` (string, liste, ou objet Nest / AllExceptionsFilter).
  static String? _flattenApiMessageField(dynamic field) {
    if (field == null) return null;
    if (field is String) {
      final t = field.trim();
      return t.isEmpty ? null : t;
    }
    if (field is List) {
      final parts = field
          .map(_flattenApiMessageField)
          .whereType<String>()
          .where((s) => s.isNotEmpty)
          .toList();
      if (parts.isEmpty) return null;
      return parts.join('. ');
    }
    if (field is Map) {
      if (field['message'] != null) {
        final inner = _flattenApiMessageField(field['message']);
        if (inner != null) return inner;
      }
      final err = field['error'];
      if (err is String && err.trim().isNotEmpty) return err.trim();
    }
    return null;
  }

  /// Extrait le message d'erreur des réponses NestJS (message string, array, ou objet).
  static String _extractErrorMessage(dynamic decoded, int statusCode) {
    const fallback = 'Erreur lors de l\'inscription';
    if (decoded == null || decoded is! Map) {
      return '$fallback ($statusCode)';
    }
    final map = decoded;
    final fromMessage = _flattenApiMessageField(map['message']);
    if (fromMessage != null) return fromMessage;
    final err = map['error'];
    if (err is String && err.trim().isNotEmpty) return err.trim();
    return '$fallback ($statusCode)';
  }

  /// Remplace « Internal server error » (souvent contrainte SQL / 500) par un texte utile à l’inscription.
  static String _humanizeParentRegistrationHttpError(String msg, int statusCode) {
    if (statusCode == 409) return msg;

    final lower = msg.toLowerCase().trim();
    final looksLikeGenericServerError = lower == 'internal server error' ||
        lower == 'internal server error.' ||
        lower.contains('internal server error');

    if (statusCode >= 500 && looksLikeGenericServerError) {
      return 'Impossible d\'enregistrer votre demande pour le moment. '
          'Souvent, cela signifie que cette adresse e-mail est déjà utilisée : '
          'connectez-vous ou utilisez une autre adresse pour le parent principal '
          '(et pour le co-parent si vous en indiquez un). '
          'Si le problème continue, réessayez plus tard ou contactez le support.';
    }

    if (statusCode >= 500) {
      if (lower.contains('duplicate') ||
          lower.contains('unique constraint') ||
          lower.contains('23505') ||
          lower.contains('already exist')) {
        return 'Cette adresse e-mail semble déjà enregistrée. '
            'Essayez de vous connecter ou modifiez l\'adresse du parent ou du co-parent.';
      }
    }
    return msg;
  }

  /// Rafraîchit le profil utilisateur depuis l'API
  static Future<AppUser?> refreshCurrentUser() async {
    final token = await TokenService.getToken();
    if (token == null) return null;

    try {
      final user = await _fetchUserProfile(token);
      await _saveCurrentUser(user);
      return user;
    } catch (e) {
      return null;
    }
  }
} 