export interface Location {
  lat: number;
  lng: number;
  address?: string;
  accuracy?: number;
}

export interface LocationWithDetails extends Location {
  commune?: string;
  district?: string;
  city?: string;
  country?: string;
}

export interface DistanceResult {
  distance: number; // in kilometers
  duration: number; // in minutes (estimated)
  bearing: number; // compass bearing in degrees
}

export interface RoutePoint {
  lat: number;
  lng: number;
  instruction?: string;
  distance?: number;
  duration?: number;
}

export interface GeolocationOptions {
  enableHighAccuracy?: boolean;
  timeout?: number;
  maximumAge?: number;
}

export class GeolocationService {
  private watchId: number | null = null;
  private lastKnownLocation: Location | null = null;

  /**
   * Get current position using browser geolocation
   */
  async getCurrentPosition(options: GeolocationOptions = {}): Promise<Location> {
    const defaultOptions: GeolocationOptions = {
      enableHighAccuracy: true,
      timeout: 10000,
      maximumAge: 300000, // 5 minutes
      ...options
    };

    return new Promise((resolve, reject) => {
      if (!navigator.geolocation) {
        reject(new Error('Géolocalisation non supportée par ce navigateur'));
        return;
      }

      navigator.geolocation.getCurrentPosition(
        async (position) => {
          const location: Location = {
            lat: position.coords.latitude,
            lng: position.coords.longitude,
            accuracy: position.coords.accuracy
          };

          // Try to get address through reverse geocoding
          try {
            const address = await this.reverseGeocode(location.lat, location.lng);
            location.address = address;
          } catch (error) {
            console.warn('Failed to get address:', error);
          }

          this.lastKnownLocation = location;
          resolve(location);
        },
        (error) => {
          const errorMessages: Record<number, string> = {
            1: 'Permission de géolocalisation refusée',
            2: 'Position indisponible',
            3: 'Timeout de géolocalisation'
          };
          reject(new Error(errorMessages[error.code] || 'Erreur de géolocalisation'));
        },
        defaultOptions
      );
    });
  }

  /**
   * Watch position changes for real-time tracking
   */
  watchPosition(
    onLocationUpdate: (location: Location) => void,
    onError: (error: Error) => void,
    options: GeolocationOptions = {}
  ): void {
    if (!navigator.geolocation) {
      onError(new Error('Géolocalisation non supportée'));
      return;
    }

    const defaultOptions: GeolocationOptions = {
      enableHighAccuracy: true,
      timeout: 10000,
      maximumAge: 60000, // 1 minute
      ...options
    };

    this.watchId = navigator.geolocation.watchPosition(
      async (position) => {
        const location: Location = {
          lat: position.coords.latitude,
          lng: position.coords.longitude,
          accuracy: position.coords.accuracy
        };

        // Try to get address if location changed significantly
        if (!this.lastKnownLocation || 
            this.calculateDistance(this.lastKnownLocation, location).distance > 0.1) {
          try {
            const address = await this.reverseGeocode(location.lat, location.lng);
            location.address = address;
          } catch (error) {
            console.warn('Failed to get address:', error);
          }
        }

        this.lastKnownLocation = location;
        onLocationUpdate(location);
      },
      (error) => {
        const errorMessages: Record<number, string> = {
          1: 'Permission de géolocalisation refusée',
          2: 'Position indisponible',
          3: 'Timeout de géolocalisation'
        };
        onError(new Error(errorMessages[error.code] || 'Erreur de géolocalisation'));
      },
      defaultOptions
    );
  }

  /**
   * Stop watching position
   */
  stopWatching(): void {
    if (this.watchId !== null) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
    }
  }

  /**
   * Calculate distance between two points using Haversine formula
   */
  calculateDistance(point1: Location, point2: Location): DistanceResult {
    const R = 6371; // Earth's radius in kilometers
    const dLat = this.toRad(point2.lat - point1.lat);
    const dLng = this.toRad(point2.lng - point1.lng);
    const a = 
      Math.sin(dLat/2) * Math.sin(dLat/2) +
      Math.cos(this.toRad(point1.lat)) * Math.cos(this.toRad(point2.lat)) * 
      Math.sin(dLng/2) * Math.sin(dLng/2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    const distance = R * c;

    // Calculate bearing
    const y = Math.sin(dLng) * Math.cos(this.toRad(point2.lat));
    const x = Math.cos(this.toRad(point1.lat)) * Math.sin(this.toRad(point2.lat)) -
              Math.sin(this.toRad(point1.lat)) * Math.cos(this.toRad(point2.lat)) * Math.cos(dLng);
    const bearing = (this.toDeg(Math.atan2(y, x)) + 360) % 360;

    // Estimate duration based on urban travel speed
    const estimatedSpeed = this.getEstimatedSpeed(distance);
    const duration = (distance / estimatedSpeed) * 60; // in minutes

    return {
      distance: Math.round(distance * 100) / 100,
      duration: Math.ceil(duration),
      bearing: Math.round(bearing)
    };
  }

  /**
   * Find nearby points within a given radius
   */
  findNearbyPoints<T extends Location & { id: string }>(
    center: Location, 
    points: T[], 
    radiusKm: number = 10
  ): Array<T & { distance: number; duration: number; bearing: number }> {
    return points
      .map(point => {
        const result = this.calculateDistance(center, point);
        return {
          ...point,
          distance: result.distance,
          duration: result.duration,
          bearing: result.bearing
        };
      })
      .filter(point => point.distance <= radiusKm)
      .sort((a, b) => a.distance - b.distance);
  }

  /**
   * Check if location is within Abidjan service area
   */
  isWithinServiceArea(location: Location): boolean {
    const abidjanBounds = {
      south: 5.2,
      north: 5.5,
      west: -4.2,
      east: -3.8
    };

    return (
      location.lat >= abidjanBounds.south &&
      location.lat <= abidjanBounds.north &&
      location.lng >= abidjanBounds.west &&
      location.lng <= abidjanBounds.east
    );
  }

  /**
   * Get Abidjan center point (Plateau district)
   */
  getAbidjanCenter(): Location {
    return {
      lat: 5.3364,
      lng: -4.0267,
      address: 'Plateau, Abidjan, Côte d\'Ivoire'
    };
  }

  /**
   * Get common Abidjan locations
   */
  getAbidjanDistricts(): (LocationWithDetails & { id: string })[] {
    return [
      { id: 'plateau', lat: 5.3364, lng: -4.0267, address: 'Plateau', commune: 'Plateau', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'cocody', lat: 5.3547, lng: -3.9749, address: 'Cocody', commune: 'Cocody', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'marcory', lat: 5.2915, lng: -3.9890, address: 'Marcory', commune: 'Marcory', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'adjame', lat: 5.3456, lng: -4.0784, address: 'Adjamé', commune: 'Adjamé', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'treichville', lat: 5.3156, lng: -4.0167, address: 'Treichville', commune: 'Treichville', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'yopougon', lat: 5.3789, lng: -4.0245, address: 'Yopougon', commune: 'Yopougon', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'koumassi', lat: 5.2845, lng: -4.0456, address: 'Koumassi', commune: 'Koumassi', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' },
      { id: 'bingerville', lat: 5.4123, lng: -3.9567, address: 'Bingerville', commune: 'Bingerville', district: 'Abidjan', city: 'Abidjan', country: 'Côte d\'Ivoire' }
    ];
  }

  /**
   * Simple reverse geocoding for Abidjan area
   */
  async reverseGeocode(lat: number, lng: number): Promise<string> {
    // For a production app, you would use a real geocoding service
    // This is a simplified version for Abidjan
    const districts = this.getAbidjanDistricts();
    const location = { lat, lng };
    
    // Find closest district
    const nearest = this.findNearbyPoints(location, districts, 50)[0];
    
    if (nearest && nearest.distance < 5) {
      return `Près de ${nearest.address}, Abidjan`;
    }

    if (this.isWithinServiceArea({ lat, lng })) {
      return 'Abidjan, Côte d\'Ivoire';
    }

    return `${lat.toFixed(4)}, ${lng.toFixed(4)}`;
  }

  /**
   * Generate a simple route between two points
   */
  generateRoute(start: Location, end: Location): RoutePoint[] {
    // This is a simplified route generation
    // In a real app, you would use a routing service like OpenRouteService or Google Directions
    const route: RoutePoint[] = [];
    
    // Add start point
    route.push({
      lat: start.lat,
      lng: start.lng,
      instruction: 'Point de départ',
      distance: 0,
      duration: 0
    });

    // Add some intermediate points for a more realistic route
    const midLat = (start.lat + end.lat) / 2;
    const midLng = (start.lng + end.lng) / 2;
    
    route.push({
      lat: midLat,
      lng: midLng,
      instruction: 'Continuez tout droit',
      distance: this.calculateDistance(start, { lat: midLat, lng: midLng }).distance,
      duration: this.calculateDistance(start, { lat: midLat, lng: midLng }).duration
    });

    // Add end point
    const totalDistance = this.calculateDistance(start, end);
    route.push({
      lat: end.lat,
      lng: end.lng,
      instruction: 'Arrivée à destination',
      distance: totalDistance.distance,
      duration: totalDistance.duration
    });

    return route;
  }

  /**
   * Format distance for display
   */
  formatDistance(distanceKm: number): string {
    if (distanceKm < 1) {
      return `${Math.round(distanceKm * 1000)}m`;
    }
    return `${distanceKm.toFixed(1)}km`;
  }

  /**
   * Format duration for display
   */
  formatDuration(minutes: number): string {
    if (minutes < 60) {
      return `${minutes}min`;
    }
    const hours = Math.floor(minutes / 60);
    const remainingMinutes = minutes % 60;
    return `${hours}h${remainingMinutes > 0 ? remainingMinutes + 'min' : ''}`;
  }

  /**
   * Get estimated speed based on distance and urban conditions
   */
  private getEstimatedSpeed(distanceKm: number): number {
    // Speed estimates for urban Abidjan
    if (distanceKm < 2) return 15; // Walking/bike speed for short distances
    if (distanceKm < 10) return 25; // Mixed traffic, urban roads
    return 35; // Highway/major roads
  }

  private toRad(value: number): number {
    return value * Math.PI / 180;
  }

  private toDeg(value: number): number {
    return value * 180 / Math.PI;
  }
}

// Singleton instance
export const geolocationService = new GeolocationService();