import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:geolocator/geolocator.dart';
import 'dart:math' as math;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:nearle/utils/kalman_filter.dart';
import 'package:nearle/utils/mqtt_service.dart';
import 'package:battery_plus/battery_plus.dart';

class LiveTrackingService {
  static final LiveTrackingService _instance = LiveTrackingService._internal();

  factory LiveTrackingService() => _instance;

  LiveTrackingService._internal();

  StreamSubscription<Position>? _positionStreamSubscription;
  bool _isTracking = false;
  bool _isProcessing = false;
  NearleKalmanFilter? _kf;
  DateTime? _lastUpdateTime;
  DateTime? _lastSentTime;
  DateTime? _lastTelemetrySent;
  Timer? _watchdogTimer;

  // Thresholds
  static const double _maxAccuracyMeters = 50.0;  // Reject GPS > 50m accuracy
  static const double _minDistanceMeters = 5.0;   // Minimum movement to count
  static const double _maxJumpMeters = 200.0;     // Reject single-step jumps > 200m
  static const int _rateLimitSeconds = 3;          // Minimum seconds between updates
  static const int _watchdogSeconds = 45;          // Restart if no update in 45s

  void startTracking() async {
    if (_isTracking) return;

    final serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      debugPrint('[LIVE TRACKING] Location services disabled.');
      return;
    }

    final permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied ||
        permission == LocationPermission.deniedForever) {
      debugPrint('[LIVE TRACKING] Location permission denied.');
      return;
    }

    _isTracking = true;
    _lastSentTime = null;
    debugPrint('[LIVE TRACKING] Starting high-frequency tracking...');

    _startStream();
    _startWatchdog();
  }

  void _startStream() {
    _positionStreamSubscription?.cancel();

    const locationSettings = LocationSettings(
      accuracy: LocationAccuracy.bestForNavigation,
      distanceFilter: 0,
    );

    _positionStreamSubscription = Geolocator.getPositionStream(
      locationSettings: locationSettings,
    ).listen(
      (Position position) async {
        await _sendToKalmanBackend(position);
      },
      onError: (error) {
        debugPrint('[LIVE TRACKING] Stream error: $error — restarting in 5s');
        _positionStreamSubscription?.cancel();
        _positionStreamSubscription = null;
        if (_isTracking) {
          Future.delayed(const Duration(seconds: 5), () {
            if (_isTracking) _startStream();
          });
        }
      },
      cancelOnError: true,
    );
  }

  void _startWatchdog() {
    _watchdogTimer?.cancel();
    _watchdogTimer = Timer.periodic(
      const Duration(seconds: _watchdogSeconds),
      (_) {
        if (!_isTracking) return;
        final lastSent = _lastSentTime;
        if (lastSent == null) return;
        final staleSeconds = DateTime.now().difference(lastSent).inSeconds;
        if (staleSeconds > _watchdogSeconds) {
          debugPrint(
            '[LIVE TRACKING] Watchdog: stream stale for ${staleSeconds}s — restarting',
          );
          _positionStreamSubscription?.cancel();
          _positionStreamSubscription = null;
          _kf = null;
          _lastUpdateTime = null;
          _startStream();
        }
      },
    );
  }

  void stopTracking() {
    if (!_isTracking) return;
    debugPrint('[LIVE TRACKING] Stopping tracking.');
    _watchdogTimer?.cancel();
    _watchdogTimer = null;
    _positionStreamSubscription?.cancel();
    _positionStreamSubscription = null;
    _kf = null;
    _lastUpdateTime = null;
    _isTracking = false;
  }

  Future<void> _sendToKalmanBackend(Position position) async {
    final now = DateTime.now();

    // Rate limiter
    if (_lastSentTime != null &&
        now.difference(_lastSentTime!).inSeconds < _rateLimitSeconds) {
      return;
    }

    // Mutex — prevent concurrent processing
    if (_isProcessing) return;
    _isProcessing = true;
    _lastSentTime = now;

    try {
      // Reject mocked GPS (anti-cheat)
      if (position.isMocked) {
        debugPrint('[LIVE TRACKING] Skipped mocked position');
        return;
      }

      // Reject poor-accuracy GPS (power saver / other apps degrading GPS)
      if (position.accuracy > _maxAccuracyMeters) {
        debugPrint(
          '[LIVE TRACKING] Skipped low-accuracy position: ${position.accuracy.toStringAsFixed(0)}m',
        );
        return;
      }

      final prefs = await SharedPreferences.getInstance();
      final userid = prefs.getInt('userId') ?? prefs.getInt('userid');
      final currentOrderId = prefs.getString('current_riding_order_id') ?? '';

      final double headingRadians = position.heading * (math.pi / 180.0);
      final double velocityLng = position.speed * math.sin(headingRadians);
      final double velocityLat = position.speed * math.cos(headingRadians);

      double displayLat = position.latitude;
      double displayLng = position.longitude;

      if (_kf == null) {
        _kf = NearleKalmanFilter(
          lat: position.latitude,
          lng: position.longitude,
        );
      } else {
        final double dt = _lastUpdateTime != null
            ? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0
            : _rateLimitSeconds.toDouble();
        _kf!.predict(dt);
        _kf!.update(position.latitude, position.longitude);
        displayLat = _kf!.x[0];
        displayLng = _kf!.x[1];
      }
      _lastUpdateTime = now;

      // Stamp that the main isolate is actively tracking.
      // The foreground service reads this to avoid double-counting KMs.
      await prefs.setInt('live_tracking_last_update_ms', now.millisecondsSinceEpoch);

      final payload = {
        'userid': userid,
        'orderid': currentOrderId,
        'lat': displayLat,
        'lng': displayLng,
        'raw_lat': position.latitude,
        'raw_lng': position.longitude,
        'speed': position.speed,
        'heading': position.heading,
        'velocity_lat': velocityLat,
        'velocity_lng': velocityLng,
        'timestamp': now.toIso8601String(),
      };

      // Accumulate cumulative KMs for active deliveries
      final activeDeliveryIds =
          prefs.getStringList('active_tracking_delivery_ids') ?? [];
      for (final dId in activeDeliveryIds) {
        try {
          final lastLatStr =
              prefs.getString('delivery_tracking_${dId}_lastLat') ?? '';
          final lastLngStr =
              prefs.getString('delivery_tracking_${dId}_lastLng') ?? '';
          final currentCumKm = double.tryParse(
                prefs.getString('delivery_tracking_${dId}_cumulativeKm') ?? '0',
              ) ??
              0.0;

          if (lastLatStr.isNotEmpty && lastLngStr.isNotEmpty) {
            final lastLat = double.tryParse(lastLatStr) ?? 0.0;
            final lastLng = double.tryParse(lastLngStr) ?? 0.0;

            if (lastLat != 0 && lastLng != 0 && displayLat != 0 && displayLng != 0) {
              final distanceMeters = Geolocator.distanceBetween(
                lastLat,
                lastLng,
                displayLat,
                displayLng,
              );

              // Speed-based jump guard: reject if implied speed > 120 km/h (33.3 m/s)
              final lastUpdateMs = prefs.getInt('delivery_tracking_${dId}_lastUpdateMs') ?? 0;
              final elapsedSeconds = lastUpdateMs > 0
                  ? (now.millisecondsSinceEpoch - lastUpdateMs) / 1000.0
                  : _rateLimitSeconds.toDouble();
              final maxRealisticMeters = elapsedSeconds * 33.3; // 120 km/h ceiling

              if (distanceMeters > maxRealisticMeters && distanceMeters > _maxJumpMeters) {
                // GPS jumped — update anchor without counting the phantom distance
                debugPrint(
                  '[LIVE TRACKING] GPS jump for $dId: ${distanceMeters.toStringAsFixed(0)}m '
                  'in ${elapsedSeconds.toStringAsFixed(1)}s (max realistic: ${maxRealisticMeters.toStringAsFixed(0)}m) — resetting anchor',
                );
                await prefs.setString('delivery_tracking_${dId}_lastLat', displayLat.toString());
                await prefs.setString('delivery_tracking_${dId}_lastLng', displayLng.toString());
                await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', now.millisecondsSinceEpoch);
              } else if (distanceMeters >= _minDistanceMeters) {
                final newCumKm = currentCumKm + (distanceMeters / 1000.0);
                await prefs.setString(
                  'delivery_tracking_${dId}_cumulativeKm',
                  newCumKm.toStringAsFixed(4),
                );
                await prefs.setString('delivery_tracking_${dId}_lastLat', displayLat.toString());
                await prefs.setString('delivery_tracking_${dId}_lastLng', displayLng.toString());
                await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', now.millisecondsSinceEpoch);
              }
              // else: too small — skip without moving anchor (avoids GPS noise accumulation)
            }
          } else {
            // First point for this delivery — set anchor only
            await prefs.setString('delivery_tracking_${dId}_lastLat', displayLat.toString());
            await prefs.setString('delivery_tracking_${dId}_lastLng', displayLng.toString());
            await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', now.millisecondsSinceEpoch);
          }
        } catch (_) {}
      }

      final mqttService = NearleMqttService();
      if (mqttService.isConnected) {
        mqttService.publishLocation(payload);
        debugPrint(
          '[LIVE TRACKING] ${displayLat.toStringAsFixed(6)}, ${displayLng.toStringAsFixed(6)} '
          '(acc: ${position.accuracy.toStringAsFixed(0)}m, speed: ${position.speed.toStringAsFixed(1)} m/s)',
        );
      } else {
        debugPrint('[LIVE TRACKING] MQTT not connected — attempting reconnect');
        mqttService.connect();
      }

      // Telemetry once every 5 minutes (timestamp guard prevents duplicate fires
      // across the 3-second GPS update window at the same minute mark)
      final shouldSendTelemetry = _lastTelemetrySent == null ||
          now.difference(_lastTelemetrySent!).inMinutes >= 5;
      if (now.minute % 5 == 0 && shouldSendTelemetry) {
        _lastTelemetrySent = now;
        try {
          final battery = Battery();
          final batteryLevel = await battery.batteryLevel;
          mqttService.publishTelemetry({
            'battery_level': batteryLevel,
            'gps_accuracy': position.accuracy,
            'mocked': position.isMocked,
            'timestamp': now.toIso8601String(),
          });
        } catch (_) {}
      }
    } catch (e) {
      debugPrint('[LIVE TRACKING] Error: $e');
    } finally {
      _isProcessing = false;
    }
  }
}
