import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import '../core/database.dart';
import '../models/trip.dart';
import '../models/photo.dart';
import '../models/place.dart';
import 'geocoding_service.dart';

class PlaceClusteringService {
  /// Earth radius in meters
  static const double _earthRadius = 6371000.0;

  /// Maximum distance to group photos into the same place (500 meters)
  static const double _maxDistanceMeters = 500.0;

  /// Maximum time difference to group photos into the same place (3 hours)
  static const int _maxTimeDifferenceMillis = 3 * 60 * 60 * 1000;

  /// Calculate distance between two GPS coordinates using Haversine formula
  static double distanceMeters(double lat1, double lon1, double lat2, double lon2) {
    final dLat = _toRadians(lat2 - lat1);
    final dLon = _toRadians(lon2 - lon1);

    final a = sin(dLat / 2) * sin(dLat / 2) +
        cos(_toRadians(lat1)) * cos(_toRadians(lat2)) * sin(dLon / 2) * sin(dLon / 2);

    final c = 2 * atan2(sqrt(a), sqrt(1 - a));
    return _earthRadius * c;
  }

  static double _toRadians(double degree) {
    return degree * pi / 180.0;
  }

  /// Runs the clustering algorithm for a trip and saves results as Place records
  static Future<List<Place>> clusterTripPlaces(Trip trip) async {
    final db = DatabaseHelper.instance;

    // Fetch existing places before we delete them to preserve custom names/descriptions
    final List<Place> existingPlaces = await db.getPlacesForTrip(trip.id);

    // 1. Clear existing places for this trip to recalculate
    await db.deletePlacesForTrip(trip.id);

    // 2. Fetch all photos for the trip
    final List<Photo> photos = await db.getPhotosForTrip(trip.id);
    if (photos.isEmpty) return [];

    // Filter photos that have GPS coordinates
    final List<Photo> gpsPhotos = photos.where((p) => p.latitude != null && p.longitude != null).toList();
    if (gpsPhotos.isEmpty) return [];

    // Sort photos chronologically by dateTaken
    gpsPhotos.sort((a, b) => (a.dateTakenUtc ?? 0).compareTo(b.dateTakenUtc ?? 0));

    // Calculate trip start time
    final tripStartMillis = trip.startTimeUtc ?? trip.createdAtUtc;

    // Helper class to represent a temporary cluster
    final List<_TempCluster> clusters = [];

    for (final Photo photo in gpsPhotos) {
      final photoLat = photo.latitude!;
      final photoLon = photo.longitude!;

      final photoTime = photo.dateTakenUtc ?? photo.dateAddedUtc ?? tripStartMillis;

      _TempCluster? bestCluster;

      // Try to find a cluster that is close in space and time
      for (final cluster in clusters) {
        final dist = distanceMeters(cluster.centerLat, cluster.centerLon, photoLat, photoLon);
        final timeDiff = (photoTime - cluster.lastTime).abs();

        if (dist <= _maxDistanceMeters && timeDiff <= _maxTimeDifferenceMillis) {
          bestCluster = cluster;
          break;
        }
      }

      if (bestCluster != null) {
        bestCluster.addPhoto(photoLat, photoLon, photoTime);
      } else {
        // Create new cluster
        final dayNumber = _calculateDayNumber(photoTime, tripStartMillis);
        clusters.add(_TempCluster(
          dayNumber: dayNumber,
          initialLat: photoLat,
          initialLon: photoLon,
          startTime: photoTime,
        ));
      }
    }

    // Convert temporary clusters to Place entities and save them
    final List<Place> places = [];
    for (var i = 0; i < clusters.length; i++) {
      final cluster = clusters[i];
      final placeId = const Uuid().v4();
      
      // Default fallback name and description
      String placeName = 'Zastávka ${i + 1}';
      String placeDescription = 'Automaticky rozpoznané místo na základě ${cluster.photoCount} fotografií.';

      // Try to match with an existing place close to this cluster's center to preserve customizations
      Place? matchingPlace;
      double minDistance = double.infinity;
      for (final existing in existingPlaces) {
        final dist = distanceMeters(existing.latitude, existing.longitude, cluster.centerLat, cluster.centerLon);
        if (dist < minDistance) {
          minDistance = dist;
          matchingPlace = existing;
        }
      }

      // If a matching place is found within 500m, preserve its name and description
      if (matchingPlace != null && minDistance <= _maxDistanceMeters) {
        placeName = matchingPlace.name ?? placeName;
        placeDescription = matchingPlace.description ?? placeDescription;
      } else {
        // Only reverse geocode if it's a completely new place cluster
        try {
          final resolvedName = await GeocodingService.reverseGeocode(cluster.centerLat, cluster.centerLon);
          if (resolvedName != null) {
            placeName = resolvedName;
          }
        } catch (e) {
          debugPrint('Chyba při automatickém vyhledávání místa: $e');
        }
      }

      final place = Place(
        id: placeId,
        tripId: trip.id,
        dayNumber: cluster.dayNumber,
        name: placeName,
        latitude: double.parse(cluster.centerLat.toStringAsFixed(6)),
        longitude: double.parse(cluster.centerLon.toStringAsFixed(6)),
        arrivalTimeUtc: cluster.startTime,
        departureTimeUtc: cluster.lastTime,
        description: placeDescription,
      );

      await db.insertPlace(place);
      places.add(place);

      // Only delay if we had to hit geocoding API for Nominatim rate limit
      if (matchingPlace == null || minDistance > _maxDistanceMeters) {
        await Future.delayed(const Duration(seconds: 1));
      }
    }

    return places;
  }

  static int _calculateDayNumber(int photoTimeMillis, int tripStartMillis) {
    final start = DateTime.fromMillisecondsSinceEpoch(tripStartMillis);
    final photo = DateTime.fromMillisecondsSinceEpoch(photoTimeMillis);

    // Calculate calendar days elapsed
    final startDay = DateTime(start.year, start.month, start.day);
    final photoDay = DateTime(photo.year, photo.month, photo.day);
    
    final elapsedDays = photoDay.difference(startDay).inDays;
    return elapsedDays + 1;
  }
}

class _TempCluster {
  final int dayNumber;
  double sumLat;
  double sumLon;
  int photoCount;
  final int startTime;
  int lastTime;

  _TempCluster({
    required this.dayNumber,
    required double initialLat,
    required double initialLon,
    required this.startTime,
  })  : sumLat = initialLat,
        sumLon = initialLon,
        photoCount = 1,
        lastTime = startTime;

  double get centerLat => sumLat / photoCount;
  double get centerLon => sumLon / photoCount;

  void addPhoto(double lat, double lon, int time) {
    sumLat += lat;
    sumLon += lon;
    photoCount++;
    lastTime = time;
  }
}
