# --- START OF FILE map_services.py ---

# map_services.py
"""
Defines the abstract base class for map tile services and concrete implementations
for specific providers like OpenStreetMap. This allows the application to
interact with different map sources through a common interface.
"""

# Standard library imports
import abc
import logging
import os
from urllib.parse import urlparse  # To check URL validity

# Third-party imports
# import requests # Will be used by the manager, not directly here usually

# --- Abstract Base Class ---


class BaseMapService(abc.ABC):
    """
    Abstract Base Class for map tile service providers.

    Defines the common interface for retrieving tile URLs and providing
    essential metadata like attribution and maximum zoom level.
    """

    # Class attributes for default values (can be overridden by subclasses)
    DEFAULT_TILE_SIZE = 256
    DEFAULT_MAX_ZOOM = 19  # A common value, adjust per service

    def __init__(
        self,
        api_key: str | None = None,
        tile_size: int = DEFAULT_TILE_SIZE,
        max_zoom: int = DEFAULT_MAX_ZOOM,
    ):
        """
        Initializes the BaseMapService.

        Args:
            api_key (str | None): API key required by the service, if any. Defaults to None.
            tile_size (int): The pixel dimension (width/height) of the map tiles. Defaults to 256.
            max_zoom (int): The maximum supported zoom level for this service. Defaults to 19.
        """
        self._log_prefix = f"[{self.__class__.__name__}]"  # Use subclass name in logs
        logging.debug(f"{self._log_prefix} Initializing map service.")
        self.api_key = api_key
        self.tile_size = tile_size
        self.max_zoom = max_zoom

        if not (isinstance(tile_size, int) and tile_size > 0):
            logging.warning(
                f"{self._log_prefix} Invalid tile_size {tile_size}. Using default {self.DEFAULT_TILE_SIZE}."
            )
            self.tile_size = self.DEFAULT_TILE_SIZE
        if not (
            isinstance(max_zoom, int) and 0 < max_zoom < 30
        ):  # Realistic zoom range
            logging.warning(
                f"{self._log_prefix} Invalid max_zoom {max_zoom}. Using default {self.DEFAULT_MAX_ZOOM}."
            )
            self.max_zoom = self.DEFAULT_MAX_ZOOM

    @property
    @abc.abstractmethod
    def name(self) -> str:
        """
        Returns the unique, short name of the map service (e.g., 'osm', 'google_roadmap').
        Used for identifying the service and potentially for cache directory naming.
        """
        pass

    @property
    @abc.abstractmethod
    def attribution(self) -> str:
        """
        Returns the required attribution text for the map service.
        This should be displayed whenever the map tiles are shown.
        """
        pass

    @abc.abstractmethod
    def get_tile_url(self, z: int, x: int, y: int) -> str | None:
        """
        Generates the URL for a specific map tile.

        Args:
            z (int): The zoom level.
            x (int): The tile X coordinate.
            y (int): The tile Y coordinate.

        Returns:
            str | None: The fully formed URL for the tile, or None if the zoom/coords
                        are invalid for this service or URL construction fails.
        """
        pass

    def validate_zoom(self, z: int) -> bool:
        """
        Checks if the requested zoom level is valid for this service.

        Args:
            z (int): The zoom level to check.

        Returns:
            bool: True if the zoom level is valid, False otherwise.
        """
        is_valid = 0 <= z <= self.max_zoom
        if not is_valid:
            logging.warning(
                f"{self._log_prefix} Requested zoom level {z} is outside the valid range [0, {self.max_zoom}]."
            )
        return is_valid

    def _validate_generated_url(self, url: str) -> bool:
        """Basic check if the generated URL seems valid."""
        try:
            result = urlparse(url)
            # Check if scheme and netloc (domain) are present
            is_valid = all([result.scheme, result.netloc])
            if not is_valid:
                logging.error(f"{self._log_prefix} Generated URL '{url}' is malformed.")
            return is_valid
        except Exception as e:
            logging.error(
                f"{self._log_prefix} Error parsing generated URL '{url}': {e}"
            )
            return False

    def __repr__(self) -> str:
        """Provides a useful representation of the service object."""
        return (
            f"{self.__class__.__name__}(name='{self.name}', max_zoom={self.max_zoom})"
        )


# --- Concrete Implementations ---


class OpenStreetMapService(BaseMapService):
    """
    Concrete implementation for fetching tiles from OpenStreetMap (OSM).
    Does not require an API key.
    """

    SERVICE_NAME = "osm"  # Unique identifier
    ATTRIBUTION_TEXT = (
        "© OpenStreetMap contributors (https://www.openstreetmap.org/copyright)"
    )
    # OSM URL template (can use subdomains a, b, c for load balancing if desired)
    URL_TEMPLATE = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
    # Known max zoom for standard OSM tile layer
    MAX_ZOOM_OSM = 19

    def __init__(self):
        """Initializes the OpenStreetMap service."""
        # Call parent init, explicitly stating no API key needed and providing OSM max zoom
        super().__init__(api_key=None, max_zoom=self.MAX_ZOOM_OSM)
        logging.info(f"{self._log_prefix} OpenStreetMap service ready.")

    @property
    def name(self) -> str:
        """Returns the service name."""
        return self.SERVICE_NAME

    @property
    def attribution(self) -> str:
        """Returns the OSM attribution text."""
        return self.ATTRIBUTION_TEXT

    def get_tile_url(self, z: int, x: int, y: int) -> str | None:
        """
        Generates the tile URL for OpenStreetMap.

        Args:
            z (int): The zoom level.
            x (int): The tile X coordinate.
            y (int): The tile Y coordinate.

        Returns:
            str | None: The tile URL or None if zoom is invalid.
        """
        # Validate zoom level using parent method
        if not self.validate_zoom(z):
            logging.warning(
                f"{self._log_prefix} Cannot generate URL for invalid zoom {z}."
            )
            return None

        # Format the URL using the class template
        try:
            url = self.URL_TEMPLATE.format(z=z, x=x, y=y)
            # Perform basic validation on the generated URL
            if not self._validate_generated_url(url):
                return None  # Error logged in validation method
            logging.debug(f"{self._log_prefix} Generated URL for ({z},{x},{y}): {url}")
            return url
        except Exception as e:
            logging.error(
                f"{self._log_prefix} Error formatting URL for ({z},{x},{y}): {e}"
            )
            return None


# --- Placeholder for future services ---

# class GoogleMapsRoadmapService(BaseMapService):
#     SERVICE_NAME = "google_roadmap"
#     ATTRIBUTION_TEXT = "© Google Maps"
#     # Example URL - Requires API Key and potentially session token, style etc.
#     # Consult Google Maps Tile API documentation for the correct format.
#     URL_TEMPLATE = "https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?key={api_key}&style=roadmap" # Example only!
#     MAX_ZOOM_GOOGLE = 22 # Example value

#     def __init__(self, api_key: str):
#         if not api_key:
#             raise ValueError("API key is required for Google Maps service.")
#         super().__init__(api_key=api_key, max_zoom=self.MAX_ZOOM_GOOGLE)
#         logging.info(f"{self._log_prefix} Google Maps Roadmap service ready.")

#     @property
#     def name(self) -> str:
#         return self.SERVICE_NAME

#     @property
#     def attribution(self) -> str:
#         return self.ATTRIBUTION_TEXT

#     def get_tile_url(self, z: int, x: int, y: int) -> str | None:
#         if not self.validate_zoom(z):
#             return None
#         try:
#             # Ensure API key is included
#             url = self.URL_TEMPLATE.format(z=z, x=x, y=y, api_key=self.api_key)
#             if not self._validate_generated_url(url):
#                 return None
#             logging.debug(f"{self._log_prefix} Generated URL for ({z},{x},{y}): {url}")
#             return url
#         except Exception as e:
#             logging.error(f"{self._log_prefix} Error formatting URL for ({z},{x},{y}): {e}")
#             return None


# --- Factory Function (Optional but helpful) ---


def get_map_service(
    service_name: str, api_key: str | None = None
) -> BaseMapService | None:
    """
    Factory function to get an instance of a specific map service.

    Args:
        service_name (str): The unique name of the service (e.g., 'osm', 'google_roadmap').
        api_key (str | None): API key, if required by the service.

    Returns:
        BaseMapService | None: An instance of the requested service, or None if not found.
    """
    log_prefix = "[MapService Factory]"
    service_name_lower = service_name.lower()
    logging.debug(f"{log_prefix} Requesting map service '{service_name_lower}'.")

    if service_name_lower == OpenStreetMapService.SERVICE_NAME:
        return OpenStreetMapService()
    # elif service_name_lower == GoogleMapsRoadmapService.SERVICE_NAME:
    #     if api_key:
    #         return GoogleMapsRoadmapService(api_key=api_key)
    #     else:
    #         logging.error(f"{log_prefix} API key required for service '{service_name}'.")
    #         return None
    # --- Add other services here ---
    # elif service_name_lower == "bing_aerial":
    #     # ... implementation ...
    #     pass
    else:
        logging.error(f"{log_prefix} Unknown map service name: '{service_name}'.")
        return None


# --- END OF FILE map_services.py ---
