/// Gestionnaire d'état pour l'analyse des cibles (ChangeNotifier).
///
/// Gère le workflow complet d'analyse : chargement d'image, détection de cible,
/// gestion des impacts (manuels et automatiques), calcul des scores,
/// analyse de groupement et sauvegarde des sessions.
library;

import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../../data/models/target_analysis.dart';
import '../../data/models/shot.dart';
import '../../data/models/target_type.dart';
import '../../data/repositories/session_repository.dart';
import '../../services/target_detection_service.dart';
import '../../services/score_calculator_service.dart';
import '../../services/grouping_analyzer_service.dart';
import '../../services/distortion_correction_service.dart';
import '../../services/opencv_target_service.dart';
import '../../services/ai_export_service.dart';

enum AnalysisState { initial, loading, success, error }

class AnalysisProvider extends ChangeNotifier {
  final TargetDetectionService _detectionService;
  final ScoreCalculatorService _scoreCalculatorService;
  final GroupingAnalyzerService _groupingAnalyzerService;
  final SessionRepository _sessionRepository;
  final DistortionCorrectionService _distortionService;
  final OpenCVTargetService _opencvTargetService;
  final Uuid _uuid = const Uuid();

  AnalysisProvider({
    required TargetDetectionService detectionService,
    required ScoreCalculatorService scoreCalculatorService,
    required GroupingAnalyzerService groupingAnalyzerService,
    required SessionRepository sessionRepository,
    DistortionCorrectionService? distortionService,
    OpenCVTargetService? opencvTargetService,
  }) : _detectionService = detectionService,
        _scoreCalculatorService = scoreCalculatorService,
        _groupingAnalyzerService = groupingAnalyzerService,
        _sessionRepository = sessionRepository,
        _distortionService = distortionService ?? DistortionCorrectionService(),
        _opencvTargetService = opencvTargetService ?? OpenCVTargetService();

  AnalysisState _state = AnalysisState.initial;
  String? _errorMessage;
  String? _imagePath;
  TargetType? _targetType;

  // AJOUT PROTECTION DU PLOTTING : Stockage permanent de la rotation du Crop
  double _cropRotation = 0.0;

  // Target detection results
  double _targetCenterX = 0.5;
  double _targetCenterY = 0.5;
  double _targetRadius = 0.4;
  double _targetInnerRadius = 0.04;
  int _ringCount = 10;
  List<double>? _ringRadii; // Individual ring radii multipliers
  double _imageAspectRatio = 1.0; // width / height

  // Shots
  List<Shot> _shots = [];

  // Score results
  ScoreResult? _scoreResult;

  // Grouping results
  GroupingResult? _groupingResult;

  // Reference-based detection
  List<Shot> _referenceImpacts = [];
  ImpactCharacteristics? _learnedCharacteristics;

  // Distortion correction
  bool _distortionCorrectionEnabled = false;
  DistortionParameters? _distortionParams;
  String? _correctedImagePath;

  // Getters
  AnalysisState get state => _state;
  String? get errorMessage => _errorMessage;
  String? get imagePath => _imagePath;
  TargetType? get targetType => _targetType;
  double get cropRotation => _cropRotation; // Getter pour le Plotting
  double get targetCenterX => _targetCenterX;
  double get targetCenterY => _targetCenterY;
  double get targetRadius => _targetRadius;
  double get targetInnerRadius => _targetInnerRadius;
  int get ringCount => _ringCount;
  List<double>? get ringRadii =>
      _ringRadii != null ? List.unmodifiable(_ringRadii!) : null;
  double get imageAspectRatio => _imageAspectRatio;
  List<Shot> get shots => List.unmodifiable(_shots);
  ScoreResult? get scoreResult => _scoreResult;
  GroupingResult? get groupingResult => _groupingResult;

  int get totalScore => _scoreResult?.totalScore ?? 0;
  int get shotCount => _shots.length;
  List<Shot> get referenceImpacts => List.unmodifiable(_referenceImpacts);
  ImpactCharacteristics? get learnedCharacteristics => _learnedCharacteristics;
  bool get hasLearnedCharacteristics => _learnedCharacteristics != null;

  // Distortion correction getters
  bool get distortionCorrectionEnabled => _distortionCorrectionEnabled;
  DistortionParameters? get distortionParams => _distortionParams;
  String? get correctedImagePath => _correctedImagePath;
  bool get hasDistortion => _distortionParams?.needsCorrection ?? false;

  /// Retourne le chemin de l'image à afficher (corrigée si activée, originale sinon)
  String? get displayImagePath =>
      _distortionCorrectionEnabled && _correctedImagePath != null
          ? _correctedImagePath
          : _imagePath;

  /// Modifie et mémorise la rotation de l'image pour le Plotting
  void setCropRotation(double rotation) {
    _cropRotation = rotation;
    notifyListeners();
  }

  /// Analyze an image
  ///
  /// [autoAnalyze] determines if we should run automatic detection immediately.
  /// If false, only the image is loaded and default target parameters are set.
  Future<void> analyzeImage(
      String imagePath,
      TargetType targetType, {
        bool autoAnalyze = true,
        Offset? manualCenter,
      }) async {
    _state = AnalysisState.loading;
    _imagePath = imagePath;
    _targetType = targetType;
    _errorMessage = null;
    notifyListeners();

    try {
      // Load image to get dimensions
      final file = File(imagePath);
      final bytes = await file.readAsBytes();
      final codec = await ui.instantiateImageCodec(bytes);
      final frame = await codec.getNextFrame();
      _imageAspectRatio = frame.image.width / frame.image.height;
      frame.image.dispose();

      if (!autoAnalyze) {
        // Just setup default values without running detection
        _targetCenterX = manualCenter?.dx ?? 0.5;
        _targetCenterY = manualCenter?.dy ?? 0.5;
        _targetRadius = 0.4;
        _targetInnerRadius = 0.04;

        // Initialize empty shots list
        _shots = [];

        _state = AnalysisState.success;
        notifyListeners();
        return;
      }

      final result = await _detectionService.detectTargetAsync(
        imagePath,
        targetType,
      );

      if (!result.success) {
        _state = AnalysisState.error;
        _errorMessage = result.errorMessage;
        notifyListeners();
        return;
      }

      _targetCenterX = result.centerX;
      _targetCenterY = result.centerY;
      _targetRadius = result.radius;
      _targetInnerRadius = result.radius * 0.1;

      // Create shots from detected impacts
      _shots = result.impacts.map((impact) {
        return Shot(
          id: _uuid.v4(),
          x: impact.x,
          y: impact.y,
          score: impact.suggestedScore,
          analysisId: '',
        );
      }).toList();

      // Calculate scores
      _recalculateScores();

      // Calculate grouping
      _recalculateGrouping();

      _state = AnalysisState.success;
      notifyListeners();
    } catch (e) {
      _state = AnalysisState.error;
      _errorMessage = 'Erreur d\'analyse: $e';
      notifyListeners();
    }
  }

  /// Add a manual shot
  void addShot(double x, double y) {
    final score = _calculateShotScore(x, y);
    final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: '');

    _shots.add(shot);
    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();
  }

  /// Remove a shot
  void removeShot(String shotId) {
    _shots.removeWhere((shot) => shot.id == shotId);
    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();
  }

  /// Move a shot to a new position
  void moveShot(String shotId, double newX, double newY) {
    final index = _shots.indexWhere((shot) => shot.id == shotId);
    if (index == -1) return;

    final newScore = _calculateShotScore(newX, newY);
    _shots[index] = _shots[index].copyWith(x: newX, y: newY, score: newScore);

    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();
  }

  /// Update a shot's score manually
  void updateShotScore(String shotId, int newScore) {
    final index = _shots.indexWhere((shot) => shot.id == shotId);
    if (index == -1) return;

    _shots[index] = _shots[index].copyWith(score: newScore);
    _recalculateScores();
    notifyListeners();
  }

  /// Auto-detect impacts using image processing
  Future<int> autoDetectImpacts({
    int darkThreshold = 80,
    int minImpactSize = 20,
    int maxImpactSize = 500,
    double minCircularity = 0.6,
    double minFillRatio = 0.5,
    bool clearExisting = false,
  }) async {
    if (_imagePath == null || _targetType == null) return 0;

    final settings = ImpactDetectionSettings(
      darkThreshold: darkThreshold,
      minImpactSize: minImpactSize,
      maxImpactSize: maxImpactSize,
      minCircularity: minCircularity,
      minFillRatio: minFillRatio,
    );

    final detectedImpacts = _detectionService.detectImpactsOnly(
      _imagePath!,
      _targetType!,
      _targetCenterX,
      _targetCenterY,
      _targetRadius,
      _ringCount,
      settings,
    );

    if (clearExisting) {
      _shots.clear();
    }

    // Add detected impacts as shots
    for (final impact in detectedImpacts) {
      final score = _calculateShotScore(impact.x, impact.y);
      final shot = Shot(
        id: _uuid.v4(),
        x: impact.x,
        y: impact.y,
        score: score,
        analysisId: '',
      );
      _shots.add(shot);
    }

    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();

    return detectedImpacts.length;
  }

  /// Auto-detect impacts using OpenCV (Hough Circles + Contours)
  Future<int> autoDetectImpactsWithOpenCV({
    double cannyThreshold1 = 50,
    double cannyThreshold2 = 150,
    double minDist = 20,
    double param1 = 100,
    double param2 = 30,
    int minRadius = 5,
    int maxRadius = 50,
    int minSize = 5,
    int maxSize = 1000,
    int blurSize = 5,
    bool useContourDetection = true,
    double minCircularity = 0.6,
    double minContourArea = 50,
    double maxContourArea = 5000,
    bool clearExisting = false,
  }) async {
    if (_imagePath == null || _targetType == null) return 0;

    final settings = OpenCVDetectionSettings(
      cannyThreshold1: cannyThreshold1,
      cannyThreshold2: cannyThreshold2,
      minDist: minDist,
      param1: param1,
      param2: param2,
      minRadius: minRadius,
      maxRadius: maxRadius,
      blurSize: blurSize,
      useContourDetection: useContourDetection,
      minCircularity: minCircularity,
      minContourArea: minContourArea,
      maxContourArea: maxContourArea,
    );

    final detectedImpacts = _detectionService.detectImpactsWithOpenCV(
      _imagePath!,
      _targetType!,
      _targetCenterX,
      _targetCenterY,
      _targetRadius,
      _ringCount,
      settings: settings,
    );

    if (clearExisting) {
      _shots.clear();
    }

    // Add detected impacts as shots
    for (final impact in detectedImpacts) {
      final score = _calculateShotScore(impact.x, impact.y);
      final shot = Shot(
        id: _uuid.v4(),
        x: impact.x,
        y: impact.y,
        score: score,
        analysisId: '',
      );
      _shots.add(shot);
    }

    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();

    return detectedImpacts.length;
  }

  /// Detect impacts with OpenCV using reference points
  Future<int> detectFromReferencesWithOpenCV({
    double tolerance = 2.0,
    bool clearExisting = false,
  }) async {
    if (_imagePath == null ||
        _targetType == null ||
        _referenceImpacts.length < 2) {
      return 0;
    }

    // Convertir les références
    final references = _referenceImpacts
        .map((shot) => ReferenceImpact(x: shot.x, y: shot.y))
        .toList();

    final detectedImpacts = _detectionService
        .detectImpactsWithOpenCVFromReferences(
      _imagePath!,
      _targetType!,
      _targetCenterX,
      _targetCenterY,
      _targetRadius,
      _ringCount,
      references,
      tolerance: tolerance,
    );

    if (clearExisting) {
      _shots.clear();
    }

    // Add detected impacts as shots
    for (final impact in detectedImpacts) {
      final score = _calculateShotScore(impact.x, impact.y);
      final shot = Shot(
        id: _uuid.v4(),
        x: impact.x,
        y: impact.y,
        score: score,
        analysisId: '',
      );
      _shots.add(shot);
    }

    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();

    return detectedImpacts.length;
  }

  /// Add a reference impact for calibrated detection
  void addReferenceImpact(double x, double y) {
    final score = _calculateShotScore(x, y);
    final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: '');
    _referenceImpacts.add(shot);
    notifyListeners();
  }

  /// Remove a reference impact
  void removeReferenceImpact(String shotId) {
    _referenceImpacts.removeWhere((shot) => shot.id == shotId);
    _learnedCharacteristics = null;
    notifyListeners();
  }

  /// Clear all reference impacts
  void clearReferenceImpacts() {
    _referenceImpacts.clear();
    _learnedCharacteristics = null;
    notifyListeners();
  }

  /// Learn characteristics from reference impacts
  bool learnFromReferences() {
    if (_imagePath == null || _referenceImpacts.length < 2) return false;

    final references = _referenceImpacts
        .map((shot) => ReferenceImpact(x: shot.x, y: shot.y))
        .toList();

    _learnedCharacteristics = _detectionService.analyzeReferenceImpacts(
      _imagePath!,
      references,
    );

    notifyListeners();
    return _learnedCharacteristics != null;
  }

  /// Auto-detect impacts using learned reference characteristics
  Future<int> detectFromReferences({
    double tolerance = 2.0,
    bool clearExisting = false,
  }) async {
    if (_imagePath == null ||
        _targetType == null ||
        _learnedCharacteristics == null) {
      return 0;
    }

    final detectedImpacts = _detectionService.detectImpactsFromReferences(
      _imagePath!,
      _targetType!,
      _targetCenterX,
      _targetCenterY,
      _targetRadius,
      _ringCount,
      _learnedCharacteristics!,
      tolerance: tolerance,
    );

    if (clearExisting) {
      _shots.clear();
    }

    // Add detected impacts as shots
    for (final impact in detectedImpacts) {
      final score = _calculateShotScore(impact.x, impact.y);
      final shot = Shot(
        id: _uuid.v4(),
        x: impact.x,
        y: impact.y,
        score: score,
        analysisId: '',
      );
      _shots.add(shot);
    }

    _recalculateScores();
    _recalculateGrouping();
    notifyListeners();

    return detectedImpacts.length;
  }

  /// Adjust target position
  void adjustTargetPosition(
      double centerX,
      double centerY,
      double innerRadius,
      double radius, {
        int? ringCount,
        List<double>? ringRadii,
        double zoomScale = 1.0,
        Offset offset = Offset.zero,
      }) {
    _targetCenterX = (centerX - offset.dx) / zoomScale;
    _targetCenterY = (centerY - offset.dy) / zoomScale;
    _targetRadius = radius / zoomScale;
    _targetInnerRadius = innerRadius / zoomScale;

    if (ringCount != null) {
      _ringCount = ringCount;
    }

    // CORRECTION : On accepte désormais la valeur null pour pouvoir réinitialiser l'espacement !
    _ringRadii = ringRadii;

    // Recalculate all shot scores based on new target position
    _shots = _shots.map((shot) {
      final newScore = _calculateShotScore(shot.x, shot.y);
      return shot.copyWith(score: newScore);
    }).toList();

    _recalculateScores();
    notifyListeners();
  }

  /// Auto-calibrate target using OpenCV
  Future<bool> autoCalibrateTarget() async {
    if (_imagePath == null) return false;

    try {
      // 1. Attempt to correct perspective/distortion first
      final correctedPath = await _distortionService
          .correctPerspectiveWithConcentricMesh(_imagePath!);

      if (correctedPath != _imagePath) {
        _imagePath = correctedPath;
        _correctedImagePath = correctedPath;
        _distortionCorrectionEnabled = true;
        _imageAspectRatio = 1.0;
        notifyListeners();
      }

      // 2. Detect the target on the straight/corrected image
      final result = await _opencvTargetService.detectTarget(_imagePath!);

      if (result.success) {
        adjustTargetPosition(
          result.centerX,
          result.centerY,
          result.radius * 0.1,
          result.radius,
        );
        return true;
      }
      return false;
    } catch (e) {
      debugPrint('Auto-calibration error: $e');
      return false;
    }
  }

  /// Calcule les paramètres de distorsion basés sur la calibration actuelle
  void calculateDistortion() {
    _distortionParams = _distortionService.calculateDistortionFromCalibration(
      targetCenterX: _targetCenterX,
      targetCenterY: _targetCenterY,
      targetRadius: _targetRadius,
      imageAspectRatio: _imageAspectRatio,
    );
    notifyListeners();
  }

  /// Applique la correction de distorsion à l'image
  /// Crée une nouvelle image corrigée et la sauvegarde
  Future<void> applyDistortionCorrection() async {
    if (_imagePath == null || _distortionParams == null) return;

    try {
      _correctedImagePath = await _distortionService.applyCorrection(
        _imagePath!,
        _distortionParams!,
      );
      _distortionCorrectionEnabled = true;
      notifyListeners();
    } catch (e) {
      _errorMessage = 'Erreur lors de la correction: $e';
      notifyListeners();
    }
  }

  /// Active ou désactive l'affichage de l'image corrigée
  void setDistortionCorrectionEnabled(bool enabled) {
    if (enabled && _correctedImagePath == null && _distortionParams != null) {
      // Si on active mais pas encore d'image corrigée, la créer
      applyDistortionCorrection();
    } else {
      _distortionCorrectionEnabled = enabled;
      notifyListeners();
    }
  }

  /// Calcule ET applique la correction pour un feedback immédiat
  Future<void> calculateAndApplyDistortion() async {
    // 1. Calcul des paramètres (votre code actuel)
    _distortionParams = _distortionService.calculateDistortionFromCalibration(
      targetCenterX: _targetCenterX,
      targetCenterY: _targetCenterY,
      targetRadius: _targetRadius,
      imageAspectRatio: _imageAspectRatio,
    );

    // 2. Vérification si une correction est réellement nécessaire
    if (_distortionParams != null && _distortionParams!.needsCorrection) {
      // 3. Application immédiate de la transformation (méthode asynchrone)
      await applyDistortionCorrection();
    } else {
      notifyListeners();
    }
  }

  Future<void> runFullDistortionWorkflow() async {
    _state = AnalysisState.loading;
    notifyListeners();

    try {
      calculateDistortion();
      await applyDistortionCorrection();
      _distortionCorrectionEnabled = true;
      _state = AnalysisState.success;
    } catch (e) {
      _errorMessage = "Erreur de rendu : $e";
      _state = AnalysisState.error;
    } finally {
      notifyListeners();
    }
  }

  int _calculateShotScore(double x, double y) {
    if (_targetType == TargetType.concentric) {
      return _scoreCalculatorService.calculateConcentricScore(
        shotX: x,
        shotY: y,
        targetCenterX: _targetCenterX,
        targetCenterY: _targetCenterY,
        targetRadius: _targetRadius,
        ringCount: _ringCount,
        imageAspectRatio: _imageAspectRatio,
        ringRadii: _ringRadii,
      );
    } else {
      return _scoreCalculatorService.calculateSilhouetteScore(
        shotX: x,
        shotY: y,
        targetCenterX: _targetCenterX,
        targetCenterY: _targetCenterY,
        targetWidth: _targetRadius * 0.8,
        targetHeight: _targetRadius * 2,
      );
    }
  }

  void _recalculateScores() {
    if (_targetType == null) return;

    _scoreResult = _scoreCalculatorService.calculateScores(
      shots: _shots,
      targetType: _targetType!,
      targetCenterX: _targetCenterX,
      targetCenterY: _targetCenterY,
      targetRadius: _targetRadius,
      ringCount: _ringCount,
      imageAspectRatio: _imageAspectRatio,
      ringRadii: _ringRadii,
    );
  }

  void _recalculateGrouping() {
    _groupingResult = _groupingAnalyzerService.analyzeGrouping(_shots);
  }

  /// Exporte l'image et le json vers le backend IA
  Future<bool> exportToAiBackend() async {
    if (_imagePath == null || _targetType == null) {
      _errorMessage = "Impossible d'export : image ou type de cible manquant.";
      notifyListeners();
      return false;
    }

    final service = AiExportService();

    _state = AnalysisState.loading;
    notifyListeners();

    final success = await service.exportData(
      imagePath: _imagePath!,
      sessionId: 'export',
      targetType: _targetType!,
      targetCenterX: _targetCenterX,
      targetCenterY: _targetCenterY,
      targetRadius: _targetRadius,
      shots: _shots,
    );

    _state = AnalysisState.success;
    if (!success) {
      _errorMessage = "Échec de l'export vers le serveur IA.";
    }
    notifyListeners();
    return success;
  }

  /// Save the session
  Future<TargetAnalysis> saveSession({
    String? notes,
    String? sessionId,
    String? weaponName,
    String? weaponId,
    int? distance,
    DateTime? date,
  }) async {
    if (_imagePath == null || _targetType == null) {
      throw Exception('Cannot save: missing image or target type');
    }

    final targetAnalysis = await _sessionRepository.prepareTargetAnalysis(
      imagePath: _imagePath!,
      sessionId: sessionId ?? 'standalone',
      targetType: _targetType!,
      shots: _shots,
      totalScore: totalScore,
      groupingDiameter: _groupingResult?.diameter,
      groupingCenterX: _groupingResult?.centerX,
      groupingCenterY: _groupingResult?.centerY,
      notes: notes,
      targetCenterX: _targetCenterX,
      targetCenterY: _targetCenterY,
      targetRadius: _targetRadius,
    );

    if (sessionId != null && sessionId != 'standalone') {
      final existingSession = await _sessionRepository.getSession(sessionId);
      if (existingSession != null) {
        await _sessionRepository.addAnalysisToSession(
          sessionId,
          targetAnalysis,
        );
      } else {
        // CORRECTION : Utilise la date injectée plutôt que DateTime.now()
        await _sessionRepository.createSession(
          id: sessionId,
          weapon: weaponName ?? 'Inconnue',
          weaponId: weaponId,
          maxShotsPerTarget: shotCount,
          analyses: [targetAnalysis],
          notes: notes,
          distance: distance ?? 25,
          date: date ?? DateTime.now(),
        );
      }
    } else {
      // CORRECTION : S'applique aussi au flux alternatif sans ID préalable
      await _sessionRepository.createSession(
        weapon: weaponName ?? 'Inconnue',
        weaponId: weaponId,
        maxShotsPerTarget: shotCount,
        analyses: [targetAnalysis],
        notes: notes,
        distance: distance ?? 25,
        date: date ?? DateTime.now(),
      );
    }

    notifyListeners();
    return targetAnalysis;
  }

  /// Reset the provider
  void reset() {
    _state = AnalysisState.initial;
    _errorMessage = null;
    _imagePath = null;
    _targetType = null;
    _cropRotation = 0.0;
    _targetCenterX = 0.5;
    _targetCenterY = 0.5;
    _targetRadius = 0.4;
    _targetInnerRadius = 0.04;
    _ringCount = 10;
    _ringRadii = null;
    _imageAspectRatio = 1.0;
    _shots = [];
    _scoreResult = null;
    _groupingResult = null;
    _referenceImpacts = [];
    _learnedCharacteristics = null;
    _distortionCorrectionEnabled = false;
    _distortionParams = null;
    _correctedImagePath = null;
    notifyListeners();
  }

  /// Met à jour la position d'un impact de tir en direct (Glisser-Déposer).
  void updateShotPosition(String shotId, double newX, double newY) {
    final index = _shots.indexWhere((s) => s.id == shotId);
    if (index != -1) {
      // 1. On calcule dynamiquement le nouveau score de la zone survolée
      final newScore = _calculateShotScore(newX, newY);

      // 2. On remplace l'ancien impact par le nouveau avec ses coordonnées et son score ajustés
      _shots[index] = _shots[index].copyWith(x: newX, y: newY, score: newScore);

      // 3. On applique les deux méthodes natives de recalcul de ton application
      _recalculateScores();
      _recalculateGrouping();

      // 4. On demande à l'écran de se redessiner en direct
      notifyListeners();
    }
  }
}