"""
Parks-specific location services with OpenStreetMap integration.
Handles geocoding, reverse geocoding, and location search for parks.
"""

import logging
from typing import Any

import requests
from django.core.cache import cache
from django.db import transaction

from ..models import ParkLocation
from apps.core.utils import capture_and_log

logger = logging.getLogger(__name__)


class ParkLocationService:
    """
    Location service specifically for parks using OpenStreetMap Nominatim API.
    """

    NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org"
    USER_AGENT = "ThrillWiki/1.0 (https://thrillwiki.com)"

    @classmethod
    def search_locations(cls, query: str, limit: int = 10) -> dict[str, Any]:
        """
        Search for locations using OpenStreetMap Nominatim API.
        Optimized for finding theme parks and amusement parks.

        Args:
            query: Search query string
            limit: Maximum number of results (default: 10, max: 25)

        Returns:
            Dictionary with search results
        """
        if not query.strip():
            return {"count": 0, "results": [], "query": query}

        # Limit the number of results
        limit = min(limit, 25)

        # Check cache first
        cache_key = f"park_location_search:{query.lower()}:{limit}"
        cached_result = cache.get(cache_key)
        if cached_result:
            return cached_result

        try:
            params = {
                "q": query,
                "format": "json",
                "limit": limit,
                "addressdetails": 1,
                "extratags": 1,
                "namedetails": 1,
                "accept-language": "en",
                # Prioritize places that might be parks or entertainment venues
                "featuretype": "settlement,leisure,tourism",
            }

            headers = {
                "User-Agent": cls.USER_AGENT,
            }

            response = requests.get(
                f"{cls.NOMINATIM_BASE_URL}/search",
                params=params,
                headers=headers,
                timeout=10,
            )
            response.raise_for_status()

            osm_results = response.json()

            # Transform OSM results to our format
            results = []
            for item in osm_results:
                result = cls._transform_osm_result(item)
                if result:
                    results.append(result)

            result_data = {"count": len(results), "results": results, "query": query}

            # Cache for 1 hour
            cache.set(cache_key, result_data, 3600)

            return result_data

        except requests.RequestException as e:
            capture_and_log(e, 'Search park locations', source='service')
            return {
                "count": 0,
                "results": [],
                "query": query,
                "error": "Location search service temporarily unavailable",
            }

    @classmethod
    def reverse_geocode(cls, latitude: float, longitude: float) -> dict[str, Any]:
        """
        Reverse geocode coordinates to get location information using OSM.

        Args:
            latitude: Latitude coordinate
            longitude: Longitude coordinate

        Returns:
            Dictionary with location information
        """
        # Validate coordinates
        if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
            return {"error": "Invalid coordinates"}

        # Check cache first
        cache_key = f"park_reverse_geocode:{latitude:.6f}:{longitude:.6f}"
        cached_result = cache.get(cache_key)
        if cached_result:
            return cached_result

        try:
            params = {
                "lat": latitude,
                "lon": longitude,
                "format": "json",
                "addressdetails": 1,
                "extratags": 1,
                "namedetails": 1,
                "accept-language": "en",
            }

            headers = {
                "User-Agent": cls.USER_AGENT,
            }

            response = requests.get(
                f"{cls.NOMINATIM_BASE_URL}/reverse",
                params=params,
                headers=headers,
                timeout=10,
            )
            response.raise_for_status()

            osm_result = response.json()

            if "error" in osm_result:
                return {"error": "Location not found"}

            result = cls._transform_osm_reverse_result(osm_result)

            # Cache for 24 hours
            cache.set(cache_key, result, 86400)

            return result

        except requests.RequestException as e:
            capture_and_log(e, 'Reverse geocode park location', source='service')
            return {"error": "Reverse geocoding service temporarily unavailable"}

    @classmethod
    def geocode_address(cls, address: str) -> dict[str, Any]:
        """
        Geocode an address to get coordinates using OSM.

        Args:
            address: Address string to geocode

        Returns:
            Dictionary with coordinates and location information
        """
        if not address.strip():
            return {"error": "Address is required"}

        # Use search_locations for geocoding
        results = cls.search_locations(address, limit=1)

        if results["count"] > 0:
            return results["results"][0]
        else:
            return {"error": "Address not found"}

    @classmethod
    def create_park_location(
        cls,
        *,
        park,
        latitude: float | None = None,
        longitude: float | None = None,
        street_address: str = "",
        city: str = "",
        state: str = "",
        country: str = "USA",
        postal_code: str = "",
        highway_exit: str = "",
        parking_notes: str = "",
        seasonal_notes: str = "",
        osm_id: int | None = None,
        osm_type: str = "",
    ) -> ParkLocation:
        """
        Create a location for a park with OSM integration.

        Args:
            park: Park instance
            latitude: Latitude coordinate
            longitude: Longitude coordinate
            street_address: Street address
            city: City name
            state: State/region name
            country: Country name (default: USA)
            postal_code: Postal/ZIP code
            highway_exit: Highway exit information
            parking_notes: Parking information
            seasonal_notes: Seasonal access notes
            osm_id: OpenStreetMap ID
            osm_type: OpenStreetMap type (node, way, relation)

        Returns:
            Created ParkLocation instance
        """
        with transaction.atomic():
            park_location = ParkLocation(
                park=park,
                street_address=street_address,
                city=city,
                state=state,
                country=country,
                postal_code=postal_code,
                highway_exit=highway_exit,
                parking_notes=parking_notes,
                seasonal_notes=seasonal_notes,
                osm_id=osm_id,
                osm_type=osm_type,
            )

            # Set coordinates if provided
            if latitude is not None and longitude is not None:
                park_location.set_coordinates(latitude, longitude)

            park_location.full_clean()
            park_location.save()

            return park_location

    @classmethod
    def update_park_location(cls, park_location: ParkLocation, **updates) -> ParkLocation:
        """
        Update park location with validation.

        Args:
            park_location: ParkLocation instance to update
            **updates: Fields to update

        Returns:
            Updated ParkLocation instance
        """
        with transaction.atomic():
            # Handle coordinates separately
            latitude = updates.pop("latitude", None)
            longitude = updates.pop("longitude", None)

            # Update regular fields
            for field, value in updates.items():
                if hasattr(park_location, field):
                    setattr(park_location, field, value)

            # Update coordinates if provided
            if latitude is not None and longitude is not None:
                park_location.set_coordinates(latitude, longitude)

            park_location.full_clean()
            park_location.save()

            return park_location

    @classmethod
    def find_nearby_parks(cls, latitude: float, longitude: float, radius_km: float = 50) -> list[ParkLocation]:
        """
        Find parks near given coordinates using PostGIS.

        Args:
            latitude: Center latitude
            longitude: Center longitude
            radius_km: Search radius in kilometers

        Returns:
            List of nearby ParkLocation instances
        """
        from django.contrib.gis.geos import Point
        from django.contrib.gis.measure import Distance

        center_point = Point(longitude, latitude, srid=4326)

        return list(
            ParkLocation.objects.filter(point__distance_lte=(center_point, Distance(km=radius_km)))
            .select_related("park", "park__operator")
            .order_by("point__distance")
        )

    @classmethod
    def enrich_location_from_osm(cls, park_location: ParkLocation) -> ParkLocation:
        """
        Enrich park location data using OSM reverse geocoding.

        Args:
            park_location: ParkLocation instance to enrich

        Returns:
            Updated ParkLocation instance
        """
        if not park_location.point:
            return park_location

        # Get detailed location info from OSM
        osm_data = cls.reverse_geocode(park_location.latitude, park_location.longitude)

        if "error" not in osm_data:
            updates = {}

            # Update missing address components
            if not park_location.street_address and osm_data.get("street_address"):
                updates["street_address"] = osm_data["street_address"]
            if not park_location.city and osm_data.get("city"):
                updates["city"] = osm_data["city"]
            if not park_location.state and osm_data.get("state"):
                updates["state"] = osm_data["state"]
            if not park_location.country and osm_data.get("country"):
                updates["country"] = osm_data["country"]
            if not park_location.postal_code and osm_data.get("postal_code"):
                updates["postal_code"] = osm_data["postal_code"]

            # Update OSM metadata
            if osm_data.get("osm_id"):
                updates["osm_id"] = osm_data["osm_id"]
            if osm_data.get("osm_type"):
                updates["osm_type"] = osm_data["osm_type"]

            if updates:
                return cls.update_park_location(park_location, **updates)

        return park_location

    @classmethod
    def _transform_osm_result(cls, osm_item: dict[str, Any]) -> dict[str, Any] | None:
        """Transform OSM search result to our standard format."""
        try:
            address = osm_item.get("address", {})

            # Extract address components
            street_number = address.get("house_number", "")
            street_name = address.get("road", "")
            street_address = f"{street_number} {street_name}".strip()

            city = (
                address.get("city")
                or address.get("town")
                or address.get("village")
                or address.get("municipality")
                or ""
            )

            state = address.get("state") or address.get("province") or address.get("region") or ""

            country = address.get("country", "")
            postal_code = address.get("postcode", "")

            # Build formatted address
            address_parts = []
            if street_address:
                address_parts.append(street_address)
            if city:
                address_parts.append(city)
            if state:
                address_parts.append(state)
            if postal_code:
                address_parts.append(postal_code)
            if country:
                address_parts.append(country)

            formatted_address = ", ".join(address_parts)

            # Check if this might be a theme park or entertainment venue
            place_type = osm_item.get("type", "").lower()
            extratags = osm_item.get("extratags", {})

            is_park_related = any(
                [
                    "park" in place_type,
                    "theme" in place_type,
                    "amusement" in place_type,
                    "attraction" in place_type,
                    extratags.get("tourism") == "theme_park",
                    extratags.get("leisure") == "amusement_arcade",
                    extratags.get("amenity") == "amusement_arcade",
                ]
            )

            return {
                "name": osm_item.get("display_name", ""),
                "latitude": float(osm_item["lat"]),
                "longitude": float(osm_item["lon"]),
                "formatted_address": formatted_address,
                "street_address": street_address,
                "city": city,
                "state": state,
                "country": country,
                "postal_code": postal_code,
                "osm_id": osm_item.get("osm_id"),
                "osm_type": osm_item.get("osm_type"),
                "place_type": place_type,
                "importance": osm_item.get("importance", 0),
                "is_park_related": is_park_related,
            }

        except (KeyError, ValueError, TypeError) as e:
            logger.warning(f"Error transforming OSM result: {str(e)}")
            return None

    @classmethod
    def _transform_osm_reverse_result(cls, osm_result: dict[str, Any]) -> dict[str, Any]:
        """Transform OSM reverse geocoding result to our standard format."""
        address = osm_result.get("address", {})

        # Extract address components
        street_number = address.get("house_number", "")
        street_name = address.get("road", "")
        street_address = f"{street_number} {street_name}".strip()

        city = address.get("city") or address.get("town") or address.get("village") or address.get("municipality") or ""

        state = address.get("state") or address.get("province") or address.get("region") or ""

        country = address.get("country", "")
        postal_code = address.get("postcode", "")

        # Build formatted address
        address_parts = []
        if street_address:
            address_parts.append(street_address)
        if city:
            address_parts.append(city)
        if state:
            address_parts.append(state)
        if postal_code:
            address_parts.append(postal_code)
        if country:
            address_parts.append(country)

        formatted_address = ", ".join(address_parts)

        return {
            "name": osm_result.get("display_name", ""),
            "latitude": float(osm_result["lat"]),
            "longitude": float(osm_result["lon"]),
            "formatted_address": formatted_address,
            "street_address": street_address,
            "city": city,
            "state": state,
            "country": country,
            "postal_code": postal_code,
            "osm_id": osm_result.get("osm_id"),
            "osm_type": osm_result.get("osm_type"),
            "place_type": osm_result.get("type", ""),
        }
