// lib/utils/fit_activity_parser.dart

import 'dart:typed_data';

import 'package:aves/model/activity/activity_track.dart';
import 'package:fit_sdk/fit_sdk.dart' as fit;
import 'package:latlong2/latlong.dart';

import 'gps_smoothing.dart';

class FitActivityParser {
  static final Distance _distance = Distance();

  static ActivityTrack parse(
    Uint8List bytes, {
    required String fileName,
  }) {
    final records = <_FitPoint>[];

    double? sessionDistanceMeters;
    double? sessionElevationGainMeters;
    Duration? sessionDuration;
    DateTime? sessionStartTime;
    String? sport;

    bool isZepp = false;

    final decoder = fit.Decode();

    decoder.onMesg = (dynamic message) {
      final messageName = '${message.name}'.toLowerCase();
      final fields = <String, dynamic>{};

      for (final dynamic field in message.fields) {
        fields['${field.name}'.toLowerCase()] = field.value;
      }

      if (_looksLikeZepp(fields)) {
        isZepp = true;
      }

      if (messageName == 'record') {
        final latRaw = _readNumber(fields, const [
          'position_lat',
          'positionLat',
          'positionlat',
          'lat',
          'latitude',
        ]);

        final lonRaw = _readNumber(fields, const [
          'position_long',
          'positionLong',
          'positionlong',
          'lon',
          'longitude',
        ]);

        if (latRaw == null || lonRaw == null) return;

        final lat = _toDegrees(latRaw);
        final lon = _toDegrees(lonRaw);

        if (!lat.isFinite || !lon.isFinite) return;
        if (lat.abs() > 90 || lon.abs() > 180) return;

        final elevation = _readNumber(fields, const [
          'enhanced_altitude',
          'enhancedAltitude',
          'enhancedaltitude',
          'altitude',
        ]);

        final time = _readDateTime(fields, const [
          'timestamp',
          'time',
        ]);

        records.add(
          _FitPoint(
            latLng: LatLng(lat, lon),
            elevationMeters: elevation,
            time: time,
            rawFields: fields,
          ),
        );
      }

      if (messageName == 'session') {
        sessionDistanceMeters ??= _readNumber(fields, const [
          'total_distance',
          'totalDistance',
          'totaldistance',
          'distance',
        ]);

        // Priorita assoluta per il D+ aggregato presente nel FIT.
        // Questo corrisponde al campo Total Ascent / total_ascent / totalAscent.
        sessionElevationGainMeters ??= _readNumber(fields, const [
          'total_ascent',
          'totalAscent',
          'totalascent',
          'ascent',
          'total_elevation_gain',
          'totalElevationGain',
          'totalElevationGainMeters',
        ]);

        final elapsedSeconds = _readNumber(fields, const [
          'total_elapsed_time',
          'totalElapsedTime',
          'total_timer_time',
          'totalTimerTime',
        ]);

        if (elapsedSeconds != null && elapsedSeconds > 0) {
          sessionDuration = Duration(seconds: elapsedSeconds.round());
        }

        sessionStartTime ??= _readDateTime(fields, const [
          'start_time',
          'startTime',
          'timestamp',
          'time',
        ]);

        final sessionSport = fields['sport'];
        if (sessionSport != null) {
          sport = _normalizeSport('$sessionSport');
        }
      }

      if (messageName == 'sport') {
        final sportValue = fields['sport'];
        if (sportValue != null) {
          sport = _normalizeSport('$sportValue');
        }
      }
    };

    decoder.read(bytes);

    final points = records
        .where((p) =>
            p.latLng.latitude.isFinite &&
            p.latLng.longitude.isFinite &&
            p.latLng.latitude.abs() <= 90 &&
            p.latLng.longitude.abs() <= 180)
        .toList();

    points.sort(_compareByTimeKeepingNullsLast);

    final originalTrack = points.map((p) => p.latLng).toList();

    // Semplifico solo la visualizzazione. Distanza e quota usano i punti originali.
    final displayTrack = isZepp
        ? GpsSmoothing.douglasPeucker(originalTrack, 5.0)
        : originalTrack;

    final computedDistanceMeters = _computeDistanceFromPoints(points, sport: sport);
    final computedElevationGainMeters = _computeElevationLikeCommercialApps(points);
    final computedDuration = _computeDuration(points);
    final computedStartTime = _computeStartTime(points);

    // Regola distanza richiesta:
    // 1) se nel FIT esiste totalDistance/total_distance plausibile, uso quello;
    // 2) altrimenti calcolo la distanza dai punti GPS con filtri anti-spike.
    final finalDistanceMeters = _chooseDistanceMeters(
      sessionDistanceMeters: sessionDistanceMeters,
      computedDistanceMeters: computedDistanceMeters,
    );

    // Regola D+ richiesta:
    // 1) se nel FIT esiste totalAscent/total_ascent plausibile, uso quello;
    // 2) altrimenti calcolo il D+ con smoothing + soglie, stile app commerciali.
    final finalElevationGainMeters = _chooseElevationGainMeters(
      sessionElevationGainMeters: sessionElevationGainMeters,
      computedElevationGainMeters: computedElevationGainMeters,
    );

    return ActivityTrack(
      name: fileName,
      format: 'FIT',
      tracks: {displayTrack},
      pointCount: points.length,
      distanceMeters: finalDistanceMeters,
      elevationGainMeters: finalElevationGainMeters,
      duration: sessionDuration ?? computedDuration,
      startTime: sessionStartTime ?? computedStartTime,
      sport: sport,
      diagnostics: [
        'isZepp=$isZepp',
        'sessionDistanceMeters=$sessionDistanceMeters',
        'computedDistanceMeters=$computedDistanceMeters',
        'finalDistanceMeters=$finalDistanceMeters',
        'sessionElevationGainMeters=$sessionElevationGainMeters',
        'computedElevationGainMeters=$computedElevationGainMeters',
        'finalElevationGainMeters=$finalElevationGainMeters',
        'elevationPointCount=${points.where((p) => p.elevationMeters != null).length}',
        'elevationMin=${_minElevation(points)}',
        'elevationMax=${_maxElevation(points)}',
        'originalPoints=${points.length}',
        'displayPoints=${displayTrack.length}',
      ].join('\n'),
    );
  }

  static bool _looksLikeZepp(Map<String, dynamic> fields) {
    final manufacturer = fields['manufacturer']?.toString().toLowerCase() ?? '';
    final product = fields['product']?.toString().toLowerCase() ?? '';
    final deviceName = fields['device_name']?.toString().toLowerCase() ?? '';
    final descriptor = fields['descriptor']?.toString().toLowerCase() ?? '';
    final serial = fields['serial_number']?.toString().toLowerCase() ?? '';
    final all = '$manufacturer $product $deviceName $descriptor $serial';

    return all.contains('zepp') ||
        all.contains('amazfit') ||
        all.contains('huami');
  }

  static double? _readNumber(Map<String, dynamic> fields, List<String> names) {
    for (final name in names) {
      final direct = fields[name];
      if (direct != null) {
        final parsed = _parseNumber(direct);
        if (parsed != null) return parsed;
      }

      final normalizedName = _normalizeFieldName(name);

      for (final entry in fields.entries) {
        if (_normalizeFieldName(entry.key) == normalizedName) {
          final parsed = _parseNumber(entry.value);
          if (parsed != null) return parsed;
        }
      }
    }

    return null;
  }

  static String _normalizeFieldName(String value) {
    return value.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
  }

  static double? _parseNumber(dynamic value) {
    if (value == null) return null;
    if (value is num) return value.toDouble();

    return double.tryParse(value.toString().replaceAll(',', '.'));
  }

  static DateTime? _readDateTime(
    Map<String, dynamic> fields,
    List<String> names,
  ) {
    for (final name in names) {
      final direct = fields[name];
      if (direct != null) {
        final parsed = _parseDateTime(direct);
        if (parsed != null) return parsed;
      }

      final normalizedName = _normalizeFieldName(name);
      for (final entry in fields.entries) {
        if (_normalizeFieldName(entry.key) == normalizedName) {
          final parsed = _parseDateTime(entry.value);
          if (parsed != null) return parsed;
        }
      }
    }

    return null;
  }

  static DateTime? _parseDateTime(dynamic value) {
    if (value == null) return null;

    if (value is DateTime) return value.toUtc();

    if (value is int && value > 0) {
      // FIT timestamp: secondi dal 1989-12-31 00:00:00 UTC.
      return DateTime.fromMillisecondsSinceEpoch(
        (value + 631065600) * 1000,
        isUtc: true,
      );
    }

    final parsed = DateTime.tryParse(value.toString());
    return parsed?.toUtc();
  }

  static double _toDegrees(num raw) {
    final v = raw.toDouble();
    if (v.abs() <= 180) return v;
    return v * 180.0 / 2147483648.0;
  }

  static double? _computeDistanceFromPoints(
    List<_FitPoint> points, {
    String? sport,
  }) {
    if (points.length < 2) return null;

    final maxSpeedMps = _maxPlausibleSpeedMps(sport);
    double dist = 0;

    for (int i = 1; i < points.length; i++) {
      final prev = points[i - 1];
      final curr = points[i];

      final seg = _distance.as(
        LengthUnit.Meter,
        prev.latLng,
        curr.latLng,
      );

      if (!seg.isFinite || seg <= 0) continue;

      final dt = prev.time != null && curr.time != null
          ? curr.time!.difference(prev.time!).inSeconds.abs()
          : null;

      if (dt == null || dt == 0) {
        if (seg > 300) continue;
        dist += seg;
        continue;
      }

      if (seg < 0.3) continue;

      final speedMps = seg / dt;
      if (speedMps > maxSpeedMps) continue;

      dist += seg;
    }

    return dist > 0 ? dist : null;
  }

  static double _maxPlausibleSpeedMps(String? sport) {
    final value = sport?.toLowerCase() ?? '';

    if (value.contains('bici') ||
        value.contains('bike') ||
        value.contains('cycling') ||
        value.contains('mountain')) {
      return 30;
    }

    if (value.contains('corsa') || value.contains('running')) {
      return 12;
    }

    if (value.contains('cammin') ||
        value.contains('walking') ||
        value.contains('trekking') ||
        value.contains('hiking')) {
      return 8;
    }

    return 25;
  }

  static double? _chooseDistanceMeters({
    required double? sessionDistanceMeters,
    required double? computedDistanceMeters,
  }) {
    final session = _normalizeDistanceMeters(sessionDistanceMeters);
    if (session != null) return session;

    return _normalizeDistanceMeters(computedDistanceMeters);
  }

  static double? _normalizeDistanceMeters(double? value) {
    if (value == null || !value.isFinite || value <= 0) return null;

    // Limite prudente: evita dati palesemente corrotti.
    // 500 km copre ultra, bici lunghe e la maggior parte delle attivita outdoor.
    const maxPlausibleMeters = 500000.0;

    // Alcuni decoder/librerie possono restituire valori FIT non scalati.
    // total_distance FIT ha scala 100: 1234567 puo significare 12345.67 m.
    if (value > maxPlausibleMeters && value / 100 <= maxPlausibleMeters) {
      return value / 100;
    }

    if (value > maxPlausibleMeters) return null;

    return value;
  }

  static double? _chooseElevationGainMeters({
    required double? sessionElevationGainMeters,
    required double? computedElevationGainMeters,
  }) {
    final session = _normalizeElevationGain(sessionElevationGainMeters);
    if (session != null) return session;

    return _normalizeElevationGain(computedElevationGainMeters);
  }

  static double? _normalizeElevationGain(double? value) {
    if (value == null || !value.isFinite || value <= 0) return null;

    // Valore molto prudente: evita solo dati evidentemente corrotti/scalati male.
    if (value > 20000) return null;

    return value;
  }

  /// Fallback quando totalAscent non esiste.
  ///
  /// Logica ispirata alle app commerciali:
  /// - rimozione valori altimetrici impossibili;
  /// - smoothing mediano leggero;
  /// - rimozione spike verticali;
  /// - somma delle salite valle-picco solo oltre una soglia minima.
  ///
  /// Non calcola mai quota finale - quota iniziale, quindi sui giri circolari
  /// il D+ non si annulla.
  static double? _computeElevationLikeCommercialApps(List<_FitPoint> points) {
    final elevations = points
        .map((p) => p.elevationMeters)
        .whereType<double>()
        .where((e) => e.isFinite && e > -500 && e < 9000)
        .toList();

    if (elevations.length < 2) return null;

    final smoothed = _medianSmooth(elevations, radius: 2);

    const minClimbMeters = 2.0;
    const reverseThresholdMeters = 2.0;
    const maxVerticalJumpMeters = 80.0;

    double gain = 0;
    double valley = smoothed.first;
    double peak = smoothed.first;
    double previous = smoothed.first;

    for (int i = 1; i < smoothed.length; i++) {
      final current = smoothed[i];
      final delta = current - previous;

      if (delta.abs() > maxVerticalJumpMeters) {
        previous = current;
        valley = current;
        peak = current;
        continue;
      }

      if (current > peak) {
        peak = current;
      }

      if (current < valley) {
        valley = current;
        peak = current;
      }

      // Quando scendo abbastanza dal picco, considero terminata una salita.
      if (peak - current >= reverseThresholdMeters) {
        final climb = peak - valley;
        if (climb >= minClimbMeters) {
          gain += climb;
        }

        valley = current;
        peak = current;
      }

      previous = current;
    }

    final finalClimb = peak - valley;
    if (finalClimb >= minClimbMeters) {
      gain += finalClimb;
    }

    return gain > 0 ? gain : null;
  }

  static List<double> _medianSmooth(List<double> values, {required int radius}) {
    if (values.length < 3 || radius <= 0) return List<double>.from(values);

    final result = <double>[];

    for (int i = 0; i < values.length; i++) {
      final start = i - radius < 0 ? 0 : i - radius;
      final end = i + radius >= values.length ? values.length - 1 : i + radius;

      final window = <double>[];
      for (int j = start; j <= end; j++) {
        window.add(values[j]);
      }
      window.sort();
      result.add(window[window.length ~/ 2]);
    }

    return result;
  }

  static Duration? _computeDuration(List<_FitPoint> points) {
    final times = points.map((p) => p.time).nonNulls.toList();
    if (times.length < 2) return null;

    times.sort();
    final duration = times.last.difference(times.first);
    return duration.isNegative ? null : duration;
  }

  static DateTime? _computeStartTime(List<_FitPoint> points) {
    final times = points.map((p) => p.time).nonNulls.toList();
    if (times.isEmpty) return null;

    times.sort();
    return times.first;
  }

  static double? _minElevation(List<_FitPoint> points) {
    final values = points
        .map((p) => p.elevationMeters)
        .whereType<double>()
        .where((e) => e.isFinite)
        .toList();

    if (values.isEmpty) return null;
    values.sort();
    return values.first;
  }

  static double? _maxElevation(List<_FitPoint> points) {
    final values = points
        .map((p) => p.elevationMeters)
        .whereType<double>()
        .where((e) => e.isFinite)
        .toList();

    if (values.isEmpty) return null;
    values.sort();
    return values.last;
  }

  static int _compareByTimeKeepingNullsLast(_FitPoint a, _FitPoint b) {
    final at = a.time;
    final bt = b.time;

    if (at == null && bt == null) return 0;
    if (at == null) return 1;
    if (bt == null) return -1;

    return at.compareTo(bt);
  }

  static double? _normalizeSuspiciousDistance(double? value) {
    return _normalizeDistanceMeters(value);
  }

  static String _normalizeSport(String raw) {
    final value = raw.trim().toLowerCase();
    final numeric = int.tryParse(value);

    if (numeric != null) {
      switch (numeric) {
        case 1:
          return 'Corsa';
        case 2:
          return 'Bici';
        case 5:
          return 'Nuoto';
        case 6:
          return 'Camminata';
        case 9:
          return 'Trekking';
        case 20:
          return 'Mountain Bike';
        default:
          return 'Sport $numeric';
      }
    }

    switch (value) {
      case 'running':
        return 'Corsa';
      case 'cycling':
        return 'Bici';
      case 'walking':
        return 'Camminata';
      case 'hiking':
        return 'Trekking';
      case 'swimming':
        return 'Nuoto';
      default:
        return raw.trim();
    }
  }
}

class _FitPoint {
  final LatLng latLng;
  final double? elevationMeters;
  final DateTime? time;
  final Map<String, dynamic> rawFields;

  const _FitPoint({
    required this.latLng,
    this.elevationMeters,
    required this.time,
    required this.rawFields,
  });
}
