// SPDX-License-Identifier: Proprietary
// SPKI (Subject Public Key Info) certificate pinning service for UnionFlow mobile.
//
// Remplace l'ancien check CN par un check de digest SHA-256 de la SPKI, conforme
// aux recommandations 2026 :
//   - Google déconseille le pinning de cert statique (rotation légitime brique l'app)
//   - Let's Encrypt 90 jours / certs commerciaux 398 jours max → cert statique = brick garanti
//   - SPKI digest survit aux rotations tant que la clé publique reste la même
//
// Pour rotation effective : la liste de pins est récupérée dynamiquement depuis Firebase
// Remote Config, ce qui permet d'ajouter de nouvelles clés (transition) sans re-publier l'app.
//
// @since 2026-04-25 (P0-NEW-21)

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

import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';

import '../utils/app_logger.dart';

/// Liste de digests SHA-256 (en base64) des SPKI valides.
///
/// Mise à jour dynamique via Firebase Remote Config (clé `spki_pins`) avec rotation
/// transparente. Format : tableau de strings base64.
class SpkiPinningService {
  SpkiPinningService._();

  static final SpkiPinningService instance = SpkiPinningService._();

  /// Digests SHA-256 (base64) à shipper en fallback statique dans le bundle —
  /// utilisés si Firebase Remote Config est indisponible au démarrage.
  ///
  /// **Avant go-live**, remplir avec :
  ///   1. Le digest de la clé publique du cert prod (api.lions.dev)
  ///   2. Le digest d'une clé de backup (à provisionner avant rotation)
  ///   3. Optionnellement : le digest d'une clé Let's Encrypt intermédiaire
  static const List<String> _fallbackPins = <String>[
    // TODO avant go-live :
    // 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // prod cert pubkey
    // 'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // backup pubkey
  ];

  /// Hosts soumis au pinning (regex acceptée).
  static const List<String> _pinnedHosts = <String>[
    'api.lions.dev',
    'security.lions.dev',
  ];

  /// Cache des pins après chargement Firebase (refresh à chaque démarrage app).
  List<String>? _activePins;

  /// Initialise le service en récupérant la liste de pins depuis Firebase Remote Config.
  /// Doit être appelé au démarrage de l'app, avant toute requête HTTPS.
  Future<void> initialize() async {
    try {
      final remoteConfig = FirebaseRemoteConfig.instance;
      await remoteConfig.setConfigSettings(RemoteConfigSettings(
        fetchTimeout: const Duration(seconds: 10),
        // Min interval = 0 en debug, 1h en prod (best practice Firebase)
        minimumFetchInterval: kReleaseMode
            ? const Duration(hours: 1)
            : Duration.zero,
      ));
      await remoteConfig.setDefaults(<String, Object>{
        'spki_pins': jsonEncode(_fallbackPins),
      });
      await remoteConfig.fetchAndActivate();

      final pinsJson = remoteConfig.getString('spki_pins');
      _activePins = (jsonDecode(pinsJson) as List).cast<String>();
      AppLogger.info(
          'SPKI pinning initialisé (${_activePins!.length} pins actifs)',
          tag: 'SpkiPinning');
    } catch (e) {
      AppLogger.warning(
          'SPKI pinning : échec Firebase Remote Config, fallback bundle ($_fallbackPins.length pins) — $e',
          tag: 'SpkiPinning');
      _activePins = _fallbackPins;
    }
  }

  /// Vérifie qu'un certificat X.509 a une SPKI dont le digest SHA-256 figure dans la liste
  /// active de pins. Retourne true si OK, false si le pin ne matche pas.
  ///
  /// Utilisé dans `HttpClient.badCertificateCallback` :
  /// ```dart
  /// client.badCertificateCallback = (cert, host, port) =>
  ///     SpkiPinningService.instance.verifyCertificate(cert, host);
  /// ```
  bool verifyCertificate(X509Certificate cert, String host) {
    if (!_isPinnedHost(host)) {
      // Hôte non soumis au pinning → comportement par défaut (validation système)
      // Note : badCertificateCallback n'est appelé QUE si la validation système a déjà échoué.
      // Donc retourner false pour respecter le rejet par défaut.
      AppLogger.warning(
          'SSL: cert refusé pour $host (host non-pinné, validation système a échoué)',
          tag: 'SpkiPinning');
      return false;
    }

    if (_activePins == null || _activePins!.isEmpty) {
      AppLogger.error(
          'SPKI pinning : aucune liste de pins active — refus par défaut',
          tag: 'SpkiPinning');
      return false;
    }

    final spkiDigest = _computeSpkiSha256Base64(cert);
    if (spkiDigest == null) {
      AppLogger.error(
          'SPKI pinning : impossible de calculer le digest pour $host',
          tag: 'SpkiPinning');
      return false;
    }

    final formatted = 'sha256/$spkiDigest';
    final matched = _activePins!.contains(formatted);
    if (!matched) {
      AppLogger.error(
          'SPKI pinning REJECT : $host digest=$formatted ne match aucun pin actif',
          tag: 'SpkiPinning');
    }
    return matched;
  }

  bool _isPinnedHost(String host) {
    final lower = host.toLowerCase();
    for (final pinned in _pinnedHosts) {
      if (lower == pinned || lower.endsWith('.$pinned')) {
        return true;
      }
    }
    return false;
  }

  /// Calcule SHA-256 de la SubjectPublicKeyInfo du certificat, encodé en base64.
  ///
  /// Note : `X509Certificate.der` retourne la DER complète du cert. Pour extraire la SPKI
  /// proprement il faudrait parser ASN.1 — pas trivial en Dart standard. Solution
  /// pragmatique : hasher l'intégralité de la DER (variant courant "Certificate Pinning"
  /// plutôt que pure SPKI). Pour le vrai SPKI, intégrer le package `asn1lib` ou
  /// `app_fortress` qui le calcule nativement.
  ///
  /// **TODO avant go-live banking-grade** : passer à `app_fortress` pour SPKI réel +
  /// hardening natif (Frida bypass).
  String? _computeSpkiSha256Base64(X509Certificate cert) {
    try {
      final der = cert.der;
      final digest = sha256.convert(der);
      return base64Encode(digest.bytes);
    } catch (e) {
      AppLogger.error('SPKI digest computation failed: $e', tag: 'SpkiPinning');
      return null;
    }
  }
}
