import 'package:dio/dio.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:injectable/injectable.dart';
import '../models/user.dart';
import 'keycloak_role_mapper.dart';
import '../../../../core/config/environment.dart';
import '../../../../core/utils/logger.dart';

/// Configuration Keycloak centralisée
class KeycloakConfig {
  static String get baseUrl => AppConfig.keycloakBaseUrl;
  static const String realm = 'unionflow';
  static const String clientId = 'unionflow-mobile';
  static const String scopes = 'openid profile email roles';

  static String get tokenEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/token';
  static String get logoutEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/logout';
}

/// Service d'Authentification Keycloak Épuré & DRY
@lazySingleton
class KeycloakAuthService {
  final Dio _dio = Dio();
  final FlutterAppAuth _appAuth = const FlutterAppAuth();
  final FlutterSecureStorage _storage = const FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
  );

  static const String _accessK = 'kc_access';
  static const String _refreshK = 'kc_refresh';
  static const String _idK = 'kc_id';

  /// Login via Authorization Code Flow + PKCE (AppAuth)
  Future<User?> loginWithAppAuth() async {
    try {
      final result = await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          KeycloakConfig.clientId,
          'dev.lions.unionflow-mobile://auth/callback',
          serviceConfiguration: AuthorizationServiceConfiguration(
            authorizationEndpoint: '${KeycloakConfig.baseUrl}/realms/${KeycloakConfig.realm}/protocol/openid-connect/auth',
            tokenEndpoint: KeycloakConfig.tokenEndpoint,
          ),
          scopes: ['openid', 'profile', 'email', 'roles', 'offline_access'],
          additionalParameters: {'kc_locale': 'fr'},
          allowInsecureConnections: true,
        ),
      );
      if (result?.accessToken != null) {
        await _saveTokens({
          'access_token': result!.accessToken,
          'refresh_token': result.refreshToken,
          'id_token': result.idToken,
        });
        return await getCurrentUser();
      }
    } catch (e, st) {
      AppLogger.error('KeycloakAuthService: auth error', error: e, stackTrace: st);
    }
    return null;
  }

  static Future<String?>? _refreshFuture;

  /// Rafraîchissement automatique du token avec verrouillage global
  Future<String?> refreshToken() async {
    if (_refreshFuture != null) {
      AppLogger.info('KeycloakAuthService: waiting for ongoing refresh');
      return await _refreshFuture;
    }

    _refreshFuture = _performRefresh();
    try {
      return await _refreshFuture;
    } finally {
      _refreshFuture = null;
    }
  }

  Future<String?> _performRefresh() async {
    final refresh = await _storage.read(key: _refreshK);
    if (refresh == null) {
      AppLogger.info('KeycloakAuthService: no refresh token available');
      return null;
    }

    try {
      AppLogger.info('KeycloakAuthService: attempting token refresh');
      final response = await _dio.post(
        KeycloakConfig.tokenEndpoint,
        data: {
          'client_id': KeycloakConfig.clientId,
          'grant_type': 'refresh_token',
          'refresh_token': refresh,
        },
        options: Options(
          contentType: Headers.formUrlEncodedContentType,
          validateStatus: (status) => status == 200,
        ),
      );

      if (response.statusCode == 200) {
        await _saveTokens(response.data);
        AppLogger.info('KeycloakAuthService: token refreshed successfully');
        return response.data['access_token'];
      }
    } on DioException catch (e, st) {
      AppLogger.error('KeycloakAuthService: refresh error ${e.response?.statusCode}', error: e, stackTrace: st);
      if (e.response?.statusCode == 400) {
        AppLogger.info('KeycloakAuthService: refresh token invalid or expired, logging out');
        await logout();
      }
    } catch (e, st) {
      AppLogger.error('KeycloakAuthService: critical refresh error', error: e, stackTrace: st);
    }
    return null;
  }

  /// Retourne un access token valide, en rafraîchissant automatiquement si expiré.
  /// Utilisé par les datasources pour éviter d'envoyer un Bearer null ou expiré.
  Future<String?> getValidAccessToken() async {
    String? token = await _storage.read(key: _accessK);
    if (token == null) return null;
    if (JwtDecoder.isExpired(token)) {
      token = await refreshToken();
    }
    return token;
  }

  /// Récupération de l'utilisateur courant + Mapage Rôles
  Future<User?> getCurrentUser() async {
    String? token = await _storage.read(key: _accessK);
    final idToken = await _storage.read(key: _idK);

    if (token == null || idToken == null) return null;

    if (JwtDecoder.isExpired(token)) {
      token = await refreshToken();
      if (token == null) return null;
    }

    try {
      final payload = JwtDecoder.decode(token);
      final idPayload = JwtDecoder.decode(idToken);

      final roles = _extractRoles(payload);
      final primaryRole = KeycloakRoleMapper.mapToUserRole(roles);
      AppLogger.info('KeycloakAuthService: roles mapped', tag: '${primaryRole.name}');

      return User(
        id: idPayload['sub'] ?? '',
        email: idPayload['email'] ?? '',
        firstName: idPayload['given_name'] ?? '',
        lastName: idPayload['family_name'] ?? '',
        primaryRole: primaryRole,
        additionalPermissions: KeycloakRoleMapper.mapToPermissions(roles),
        isActive: true,
        lastLoginAt: DateTime.now(),
        createdAt: DateTime.now(),
      );
    } catch (e, st) {
      AppLogger.error('KeycloakAuthService: user parse error', error: e, stackTrace: st);
    }
    return null;
  }

  /// Logout : révoque la session SSO côté Keycloak via le back-channel
  /// (POST /logout silencieux, sans navigateur), puis purge le stockage local.
  ///
  /// Conforme OIDC RP-Initiated Logout. Ne lève jamais d'exception : la purge
  /// locale est garantie même si Keycloak est injoignable. Le statut du
  /// back-channel est tracé dans les logs pour diagnostic.
  Future<void> logout() async {
    final refresh = await _storage.read(key: _refreshK);

    if (refresh == null || refresh.isEmpty) {
      AppLogger.info('KeycloakAuthService: no refresh token, skipping backchannel logout');
    } else {
      try {
        final response = await _dio.post(
          KeycloakConfig.logoutEndpoint,
          data: {
            'client_id': KeycloakConfig.clientId,
            'refresh_token': refresh,
          },
          options: Options(
            contentType: Headers.formUrlEncodedContentType,
            // Accepte tout statut < 600 — on interprète nous-mêmes ci-dessous.
            validateStatus: (_) => true,
          ),
        );
        final code = response.statusCode ?? 0;
        if (code == 200 || code == 204) {
          AppLogger.info('KeycloakAuthService: SSO session revoked at Keycloak (HTTP $code)');
        } else if (code == 400) {
          // Refresh token déjà invalide côté Keycloak → idempotent, OK.
          AppLogger.info('KeycloakAuthService: refresh token already invalid (HTTP 400) — session considered revoked');
        } else {
          AppLogger.error(
            'KeycloakAuthService: backchannel logout returned HTTP $code — '
            'SSO session may still be active. Body: ${response.data}',
          );
        }
      } catch (e, st) {
        AppLogger.error(
          'KeycloakAuthService: backchannel logout network error — local logout will still proceed',
          error: e,
          stackTrace: st,
        );
      }
    }

    // Purge locale toujours effectuée — l'utilisateur "se sent" déconnecté
    // immédiatement même si le serveur n'a pas pu être notifié.
    await _storage.deleteAll();
    AppLogger.info('KeycloakAuthService: local session cleared');
  }

  Future<void> _saveTokens(Map<String, dynamic> data) async {
    if (data['access_token'] != null) await _storage.write(key: _accessK, value: data['access_token']);
    if (data['refresh_token'] != null) await _storage.write(key: _refreshK, value: data['refresh_token']);
    if (data['id_token'] != null) await _storage.write(key: _idK, value: data['id_token']);
  }

  List<String> _extractRoles(Map<String, dynamic> payload) {
    final roles = <String>[];
    if (payload['realm_access']?['roles'] != null) {
      roles.addAll((payload['realm_access']['roles'] as List).cast<String>());
    }
    if (payload['resource_access'] != null) {
      (payload['resource_access'] as Map).values.forEach((v) {
        if (v['roles'] != null) roles.addAll((v['roles'] as List).cast<String>());
      });
    }
    return roles.where((r) => !r.startsWith('default-roles-') && r != 'offline_access').toList();
  }

  Future<String?> getValidToken() async {
    final token = await _storage.read(key: _accessK);
    if (token != null && !JwtDecoder.isExpired(token)) return token;
    return await refreshToken();
  }

  /// Vérifie le statut du compte sur le backend UnionFlow.
  ///
  /// Retourne un [AuthStatusResult] enrichi avec l'état d'onboarding,
  /// ou `null` en cas d'erreur réseau (on ne bloque pas en cas de doute).
  Future<AuthStatusResult?> getAuthStatus(String apiBaseUrl) async {
    try {
      final token = await getValidToken();
      if (token == null) return null;
      final response = await _dio.get(
        '$apiBaseUrl/api/membres/mon-statut',
        options: Options(
          headers: {'Authorization': 'Bearer $token'},
          validateStatus: (s) => s != null && s < 500,
        ),
      );
      if (response.statusCode == 200 && response.data is Map) {
        final data = response.data as Map;
        return AuthStatusResult(
          statutCompte: (data['statutCompte'] as String?) ?? 'ACTIF',
          onboardingState: (data['onboardingState'] as String?) ?? 'NO_SUBSCRIPTION',
          souscriptionId: data['souscriptionId'] as String?,
          waveSessionId: data['waveSessionId'] as String?,
          organisationId: data['organisationId'] as String?,
          typeOrganisation: data['typeOrganisation'] as String?,
          premierLoginComplet: (data['premierLoginComplet'] as bool?) ?? false,
          reAuthRequired: (data['reAuthRequired'] as bool?) ?? false,
        );
      }
    } catch (e) {
      AppLogger.warning('KeycloakAuthService: impossible de vérifier statut compte: $e');
    }
    return null;
  }
}

/// Résultat enrichi de /api/membres/mon-statut
class AuthStatusResult {
  final String statutCompte;
  final String onboardingState;
  final String? souscriptionId;
  final String? waveSessionId;
  final String? organisationId;
  final String? typeOrganisation;
  /// true si le premier login vient d'être complété (token à rafraîchir pour avoir MEMBRE/MEMBRE_ACTIF)
  final bool premierLoginComplet;
  /// true si une réauthentification est requise (UPDATE_PASSWORD vient d'être assigné sur un ancien compte)
  final bool reAuthRequired;

  const AuthStatusResult({
    required this.statutCompte,
    required this.onboardingState,
    this.souscriptionId,
    this.waveSessionId,
    this.organisationId,
    this.typeOrganisation,
    this.premierLoginComplet = false,
    this.reAuthRequired = false,
  });

  bool get isActive => statutCompte == 'ACTIF';
  bool get isPendingOnboarding => statutCompte == 'EN_ATTENTE_VALIDATION';
  bool get isBlocked => statutCompte == 'SUSPENDU' || statutCompte == 'DESACTIVE';
}
