"""
Abstract Base Map Provider
Defines interface for all map provider implementations
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional, Tuple
from datetime import datetime


@dataclass
class GeocodingResult:
    """Geocoding result from any provider"""
    latitude: float
    longitude: float
    address: str
    place_id: Optional[str] = None
    city: Optional[str] = None
    state: Optional[str] = None
    country: Optional[str] = None
    postal_code: Optional[str] = None
    formatted_address: Optional[str] = None
    
    def to_dict(self) -> dict:
        return {
            "latitude": self.latitude,
            "longitude": self.longitude,
            "address": self.address,
            "place_id": self.place_id,
            "city": self.city,
            "state": self.state,
            "country": self.country,
            "postal_code": self.postal_code,
            "formatted_address": self.formatted_address,
        }


@dataclass
class RouteResult:
    """Route calculation result"""
    distance_km: float
    duration_minutes: int
    polyline: str  # Encoded polyline
    steps: Optional[List[dict]] = None
    start_location: Optional[Tuple[float, float]] = None
    end_location: Optional[Tuple[float, float]] = None
    
    def to_dict(self) -> dict:
        return {
            "distance_km": self.distance_km,
            "duration_minutes": self.duration_minutes,
            "polyline": self.polyline,
            "steps": self.steps,
            "start_location": self.start_location,
            "end_location": self.end_location,
        }


@dataclass
class DistanceResult:
    """Distance and ETA result"""
    distance_km: float
    duration_minutes: int
    origin: Tuple[float, float]
    destination: Tuple[float, float]
    
    def to_dict(self) -> dict:
        return {
            "distance_km": self.distance_km,
            "duration_minutes": self.duration_minutes,
            "origin": self.origin,
            "destination": self.destination,
        }


class MapProvider(ABC):
    """
    Abstract base class for map providers.
    All map provider implementations must inherit from this class.
    """
    
    def __init__(self, api_key: str, api_url: Optional[str] = None, settings: Optional[dict] = None):
        """
        Initialize map provider with credentials.
        
        Args:
            api_key: API key for the provider
            api_url: Optional custom API URL
            settings: Optional provider-specific settings
        """
        self.api_key = api_key
        self.api_url = api_url
        self.settings = settings or {}
    
    @property
    @abstractmethod
    def provider_name(self) -> str:
        """Return provider name identifier"""
        pass
    
    @abstractmethod
    async def geocode(self, address: str) -> Optional[GeocodingResult]:
        """
        Convert address to coordinates.
        
        Args:
            address: Human-readable address
            
        Returns:
            GeocodingResult or None if not found
        """
        pass
    
    @abstractmethod
    async def reverse_geocode(self, latitude: float, longitude: float) -> Optional[GeocodingResult]:
        """
        Convert coordinates to address.
        
        Args:
            latitude: Latitude coordinate
            longitude: Longitude coordinate
            
        Returns:
            GeocodingResult or None if not found
        """
        pass
    
    @abstractmethod
    async def calculate_distance(
        self,
        origin_lat: float,
        origin_lng: float,
        dest_lat: float,
        dest_lng: float
    ) -> Optional[DistanceResult]:
        """
        Calculate distance and ETA between two points.
        
        Args:
            origin_lat: Origin latitude
            origin_lng: Origin longitude
            dest_lat: Destination latitude
            dest_lng: Destination longitude
            
        Returns:
            DistanceResult or None on failure
        """
        pass
    
    @abstractmethod
    async def get_route(
        self,
        origin_lat: float,
        origin_lng: float,
        dest_lat: float,
        dest_lng: float,
        waypoints: Optional[List[Tuple[float, float]]] = None
    ) -> Optional[RouteResult]:
        """
        Get driving route between two points.
        
        Args:
            origin_lat: Origin latitude
            origin_lng: Origin longitude
            dest_lat: Destination latitude
            dest_lng: Destination longitude
            waypoints: Optional intermediate waypoints
            
        Returns:
            RouteResult or None on failure
        """
        pass
    
    @abstractmethod
    async def search_places(
        self,
        query: str,
        latitude: Optional[float] = None,
        longitude: Optional[float] = None,
        limit: int = 10
    ) -> List[GeocodingResult]:
        """
        Search for places by query.
        
        Args:
            query: Search query
            latitude: Optional center latitude for biasing
            longitude: Optional center longitude for biasing
            limit: Maximum number of results
            
        Returns:
            List of GeocodingResult
        """
        pass
    
    async def calculate_eta(
        self,
        origin_lat: float,
        origin_lng: float,
        dest_lat: float,
        dest_lng: float
    ) -> Optional[int]:
        """
        Calculate ETA in minutes between two points.
        Default implementation uses calculate_distance.
        
        Returns:
            ETA in minutes or None
        """
        result = await self.calculate_distance(origin_lat, origin_lng, dest_lat, dest_lng)
        return result.duration_minutes if result else None
    
    async def test_connection(self) -> Tuple[bool, Optional[str]]:
        """
        Test API connection with a simple request.
        
        Returns:
            Tuple of (success, error_message)
        """
        try:
            result = await self.geocode("New York, NY")
            if result:
                return True, None
            return False, "Geocoding returned no results"
        except Exception as e:
            return False, str(e)
    
    @staticmethod
    def haversine_distance(
        lat1: float, lon1: float,
        lat2: float, lon2: float
    ) -> float:
        """
        Calculate straight-line distance between two points (in km).
        Uses Haversine formula.
        """
        import math
        
        R = 6371  # Earth's radius in km
        
        lat1_rad = math.radians(lat1)
        lat2_rad = math.radians(lat2)
        delta_lat = math.radians(lat2 - lat1)
        delta_lon = math.radians(lon2 - lon1)
        
        a = (
            math.sin(delta_lat / 2) ** 2 +
            math.cos(lat1_rad) * math.cos(lat2_rad) *
            math.sin(delta_lon / 2) ** 2
        )
        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
        
        return R * c
