"""
GPS Coordinate Validation

Based on Wigle Android client GPS validation patterns
See docs/wigle_analysis.md Section 3 for details
"""

from dataclasses import dataclass
from typing import Optional, Tuple
from datetime import datetime
from loguru import logger


# =============================================================================
# GPS VALIDATION THRESHOLDS (Wigle-derived)
# =============================================================================

# Maximum GPS accuracy threshold (meters)
# Wigle uses 32m, we use 50m for broader acceptance
MAX_GPS_ACCURACY = 50.0

# Minimum GPS accuracy for high-quality captures
MIN_GPS_ACCURACY = 10.0

# Null Island detection threshold (degrees)
# Coordinates within this distance of (0,0) are rejected
NULL_ISLAND_THRESHOLD = 0.001

# Valid coordinate ranges
MIN_LATITUDE = -90.0
MAX_LATITUDE = 90.0
MIN_LONGITUDE = -180.0
MAX_LONGITUDE = 180.0


# =============================================================================
# DATA CLASSES
# =============================================================================

@dataclass
class GPSCoordinate:
    """GPS coordinate with metadata"""

    latitude: float
    longitude: float
    altitude: Optional[float] = None
    accuracy: Optional[float] = None
    timestamp: Optional[datetime] = None
    provider: Optional[str] = None  # 'gps', 'network', 'fused'

    def __post_init__(self):
        """Validate after initialization"""
        if not validate_gps_coordinates(self.latitude, self.longitude, self.accuracy):
            raise ValueError(
                f"Invalid GPS coordinates: lat={self.latitude}, lon={self.longitude}, "
                f"accuracy={self.accuracy}"
            )

    def to_dict(self):
        """Convert to dictionary"""
        return {
            'latitude': self.latitude,
            'longitude': self.longitude,
            'altitude': self.altitude,
            'accuracy': self.accuracy,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None,
            'provider': self.provider
        }

    def is_high_quality(self) -> bool:
        """Check if this is a high-quality GPS fix"""
        if self.accuracy is None:
            return False
        return self.accuracy <= MIN_GPS_ACCURACY


# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================

def validate_gps_coordinates(
    latitude: float,
    longitude: float,
    accuracy: Optional[float] = None
) -> bool:
    """
    Validate GPS coordinates using Wigle patterns

    Checks:
    1. Valid coordinate ranges (-90 to 90, -180 to 180)
    2. Null Island detection (reject 0.0, 0.0)
    3. Accuracy threshold (< 50 meters)

    Args:
        latitude: Latitude in decimal degrees
        longitude: Longitude in decimal degrees
        accuracy: GPS accuracy in meters (optional)

    Returns:
        True if valid, False otherwise

    Example:
        >>> validate_gps_coordinates(40.7128, -74.0060, 5.0)
        True
        >>> validate_gps_coordinates(0.0, 0.0)
        False
        >>> validate_gps_coordinates(40.7128, -74.0060, 100.0)
        False
    """

    # Check coordinate ranges
    if not (MIN_LATITUDE <= latitude <= MAX_LATITUDE):
        logger.warning(f"Latitude out of range: {latitude}")
        return False

    if not (MIN_LONGITUDE <= longitude <= MAX_LONGITUDE):
        logger.warning(f"Longitude out of range: {longitude}")
        return False

    # Check for Null Island (0, 0)
    if is_null_island(latitude, longitude):
        logger.warning(f"Null Island detected: ({latitude}, {longitude})")
        return False

    # Check accuracy threshold
    if accuracy is not None and not is_valid_accuracy(accuracy):
        logger.warning(f"GPS accuracy too low: {accuracy}m")
        return False

    return True


def is_null_island(latitude: float, longitude: float) -> bool:
    """
    Check if coordinates are at Null Island (0, 0)

    Null Island is the point where the prime meridian and equator intersect.
    GPS errors often result in (0.0, 0.0) coordinates, so we reject these.

    Args:
        latitude: Latitude in decimal degrees
        longitude: Longitude in decimal degrees

    Returns:
        True if coordinates are near (0, 0), False otherwise
    """
    return (abs(latitude) < NULL_ISLAND_THRESHOLD and
            abs(longitude) < NULL_ISLAND_THRESHOLD)


def is_valid_accuracy(accuracy: float) -> bool:
    """
    Check if GPS accuracy is within acceptable threshold

    Wigle uses 32m threshold. We use 50m for broader acceptance.

    Args:
        accuracy: GPS accuracy in meters

    Returns:
        True if accuracy is acceptable, False otherwise
    """
    return 0 < accuracy <= MAX_GPS_ACCURACY


def check_coordinate_bounds(latitude: float, longitude: float) -> Tuple[bool, Optional[str]]:
    """
    Check if coordinates are within valid bounds

    Args:
        latitude: Latitude in decimal degrees
        longitude: Longitude in decimal degrees

    Returns:
        Tuple of (is_valid, error_message)
    """
    if not (MIN_LATITUDE <= latitude <= MAX_LATITUDE):
        return False, f"Latitude {latitude} out of range [{MIN_LATITUDE}, {MAX_LATITUDE}]"

    if not (MIN_LONGITUDE <= longitude <= MAX_LONGITUDE):
        return False, f"Longitude {longitude} out of range [{MIN_LONGITUDE}, {MAX_LONGITUDE}]"

    return True, None


def anonymize_gps(
    latitude: float,
    longitude: float,
    precision_meters: int = 100
) -> Tuple[float, float]:
    """
    Anonymize GPS coordinates by reducing precision

    Wigle pattern: Allow users to control GPS precision for privacy

    Args:
        latitude: Original latitude
        longitude: Original longitude
        precision_meters: Desired precision in meters (10, 100, 1000)

    Returns:
        Tuple of (anonymized_lat, anonymized_lon)

    Example:
        >>> anonymize_gps(40.7128456, -74.0059728, 100)
        (40.71, -74.01)
    """
    # Rough conversion: 1 degree ≈ 111 km
    # 100m precision ≈ 0.001 degrees
    # 1000m precision ≈ 0.01 degrees

    if precision_meters <= 10:
        decimals = 5  # ~1.1m
    elif precision_meters <= 100:
        decimals = 3  # ~111m
    elif precision_meters <= 1000:
        decimals = 2  # ~1.1km
    else:
        decimals = 1  # ~11km

    lat_anon = round(latitude, decimals)
    lon_anon = round(longitude, decimals)

    return lat_anon, lon_anon


def calculate_distance(
    lat1: float, lon1: float,
    lat2: float, lon2: float
) -> float:
    """
    Calculate distance between two GPS coordinates using Haversine formula

    Args:
        lat1: First latitude
        lon1: First longitude
        lat2: Second latitude
        lon2: Second longitude

    Returns:
        Distance in kilometers

    Example:
        >>> calculate_distance(40.7128, -74.0060, 40.7614, -73.9776)
        8.67  # ~8.67 km from downtown Manhattan to Central Park
    """
    from math import radians, sin, cos, sqrt, atan2

    R = 6371.0  # Earth radius in kilometers

    lat1_rad = radians(lat1)
    lon1_rad = radians(lon1)
    lat2_rad = radians(lat2)
    lon2_rad = radians(lon2)

    dlat = lat2_rad - lat1_rad
    dlon = lon2_rad - lon1_rad

    a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))

    distance = R * c
    return distance


# =============================================================================
# GPS VALIDATOR CLASS
# =============================================================================

class GPSValidator:
    """
    GPS validator with configurable thresholds

    Based on Wigle's multi-level validation approach
    """

    def __init__(
        self,
        max_accuracy: float = MAX_GPS_ACCURACY,
        min_accuracy: float = MIN_GPS_ACCURACY,
        allow_null_island: bool = False,
        strict_mode: bool = False
    ):
        """
        Initialize GPS validator

        Args:
            max_accuracy: Maximum acceptable accuracy (meters)
            min_accuracy: Minimum accuracy for high-quality captures
            allow_null_island: Allow (0, 0) coordinates (not recommended)
            strict_mode: Reject coordinates without accuracy data
        """
        self.max_accuracy = max_accuracy
        self.min_accuracy = min_accuracy
        self.allow_null_island = allow_null_island
        self.strict_mode = strict_mode

        # Statistics
        self.total_validated = 0
        self.total_accepted = 0
        self.total_rejected = 0
        self.rejection_reasons = {}

    def validate(
        self,
        latitude: float,
        longitude: float,
        accuracy: Optional[float] = None
    ) -> Tuple[bool, Optional[str]]:
        """
        Validate GPS coordinates

        Args:
            latitude: Latitude in decimal degrees
            longitude: Longitude in decimal degrees
            accuracy: GPS accuracy in meters (optional)

        Returns:
            Tuple of (is_valid, rejection_reason)
        """
        self.total_validated += 1

        # Check bounds
        is_valid, error = check_coordinate_bounds(latitude, longitude)
        if not is_valid:
            self.total_rejected += 1
            self._record_rejection("out_of_bounds")
            return False, error

        # Check Null Island (unless allowed)
        if not self.allow_null_island and is_null_island(latitude, longitude):
            self.total_rejected += 1
            self._record_rejection("null_island")
            return False, "Null Island (0.0, 0.0) detected"

        # Check accuracy
        if self.strict_mode and accuracy is None:
            self.total_rejected += 1
            self._record_rejection("missing_accuracy")
            return False, "Missing accuracy data (strict mode)"

        if accuracy is not None and not (0 < accuracy <= self.max_accuracy):
            self.total_rejected += 1
            self._record_rejection("poor_accuracy")
            return False, f"GPS accuracy {accuracy}m exceeds threshold {self.max_accuracy}m"

        # All checks passed
        self.total_accepted += 1
        return True, None

    def validate_coordinate(self, coord: GPSCoordinate) -> Tuple[bool, Optional[str]]:
        """
        Validate a GPSCoordinate object

        Args:
            coord: GPSCoordinate object

        Returns:
            Tuple of (is_valid, rejection_reason)
        """
        return self.validate(coord.latitude, coord.longitude, coord.accuracy)

    def _record_rejection(self, reason: str):
        """Record rejection reason for statistics"""
        self.rejection_reasons[reason] = self.rejection_reasons.get(reason, 0) + 1

    def get_statistics(self) -> dict:
        """Get validation statistics"""
        return {
            'total_validated': self.total_validated,
            'total_accepted': self.total_accepted,
            'total_rejected': self.total_rejected,
            'acceptance_rate': (
                self.total_accepted / self.total_validated
                if self.total_validated > 0 else 0.0
            ),
            'rejection_reasons': self.rejection_reasons
        }

    def reset_statistics(self):
        """Reset validation statistics"""
        self.total_validated = 0
        self.total_accepted = 0
        self.total_rejected = 0
        self.rejection_reasons = {}


# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================

def create_gps_coordinate(
    latitude: float,
    longitude: float,
    altitude: Optional[float] = None,
    accuracy: Optional[float] = None,
    timestamp: Optional[datetime] = None,
    provider: Optional[str] = None
) -> Optional[GPSCoordinate]:
    """
    Create a validated GPSCoordinate

    Args:
        latitude: Latitude in decimal degrees
        longitude: Longitude in decimal degrees
        altitude: Altitude in meters (optional)
        accuracy: GPS accuracy in meters (optional)
        timestamp: Capture timestamp (optional)
        provider: GPS provider (optional)

    Returns:
        GPSCoordinate if valid, None otherwise
    """
    try:
        return GPSCoordinate(
            latitude=latitude,
            longitude=longitude,
            altitude=altitude,
            accuracy=accuracy,
            timestamp=timestamp,
            provider=provider
        )
    except ValueError as e:
        logger.warning(f"Failed to create GPS coordinate: {e}")
        return None


# =============================================================================
# TESTING
# =============================================================================

if __name__ == "__main__":
    # Test validation
    print("GPS Validator Tests")
    print("=" * 60)

    # Test cases
    test_cases = [
        # (lat, lon, accuracy, expected_valid, description)
        (40.7128, -74.0060, 5.0, True, "NYC - high quality"),
        (40.7128, -74.0060, 25.0, True, "NYC - acceptable quality"),
        (40.7128, -74.0060, 100.0, False, "NYC - poor accuracy"),
        (0.0, 0.0, 5.0, False, "Null Island"),
        (91.0, 0.0, 5.0, False, "Latitude out of range"),
        (0.0, 181.0, 5.0, False, "Longitude out of range"),
        (51.5074, -0.1278, 8.0, True, "London - high quality"),
    ]

    validator = GPSValidator()

    for lat, lon, acc, expected, desc in test_cases:
        is_valid, reason = validator.validate(lat, lon, acc)
        status = "✅" if is_valid == expected else "❌"
        print(f"{status} {desc}")
        print(f"   lat={lat}, lon={lon}, accuracy={acc}m")
        print(f"   Valid: {is_valid}, Reason: {reason}")
        print()

    # Print statistics
    print("Validation Statistics:")
    print("=" * 60)
    stats = validator.get_statistics()
    for key, value in stats.items():
        print(f"{key}: {value}")
