"""
What3Words grid implementation.

Provides integration with What3Words 3-meter square grid system.
Note: This implementation provides the grid structure without requiring API access.
For full What3Words functionality including word-to-coordinate conversion,
use the official What3Words API.
"""

import hashlib
import math
from typing import Any, override

from shapely.geometry import Polygon

from .base import BaseGrid, GridCell
from .cache import cached_method, cell_cache_key, geo_cache_key
from .projection_utils import get_utm_epsg_code


class What3WordsGrid(BaseGrid):
    """
    What3Words-style spatial grid system.

    Implements a 3-meter square grid system similar to What3Words.
    Each cell represents approximately a 3x3 meter square on the Earth's surface.

    Note: This implementation provides the grid structure. For actual What3Words
    integration with their word system, use the official What3Words API.
    """

    def __init__(self, precision: int = 1):
        """
        Initialize What3WordsGrid.

        Parameters
        ----------
        precision : int, optional
            Grid precision level (1 only supported for 3m squares), by default 1

        Raises
        ------
        ValueError
            If precision is not 1
        """
        if precision != 1:
            raise ValueError(
                "What3Words grid only supports precision level 1 (3m squares)"
            )
        super().__init__(precision)

    @property
    def area_km2(self) -> float:
        """
        Get the theoretical area of What3Words cells in square kilometers.

        Returns
        -------
        float
            Theoretical area in square kilometers (approximately 0.000009 km²)
        """
        return 9.0 / 1_000_000  # 3m x 3m = 9 m² = 0.000009 km²

    def _lat_lon_to_grid_coords(self, lat: float, lon: float) -> tuple[int, int]:
        """
        Convert latitude/longitude to grid coordinates.

        Parameters
        ----------
        lat : float
            Latitude
        lon : float
            Longitude

        Returns
        -------
        tuple[int, int]
            Grid coordinates (x, y)
        """
        # Earth circumference is approximately 40,075,000 meters
        # At equator, 1 degree longitude = 111,320 meters
        # 3-meter grid means each cell is approximately 3/111320 = 0.00002696 degrees

        # Calculate grid size in degrees (approximate)
        grid_size_degrees = 3.0 / 111320.0  # ~0.00002696 degrees

        # Convert to grid coordinates (floor to handle negative coords correctly)
        y = math.floor(lat / grid_size_degrees)

        # Use the row's latitude center to compute longitude grid size
        min_lat = y * grid_size_degrees
        max_lat = (y + 1) * grid_size_degrees
        lat_center = (min_lat + max_lat) / 2

        lat_correction = math.cos(math.radians(lat_center))
        lon_grid_size = (
            grid_size_degrees / lat_correction
            if lat_correction != 0
            else grid_size_degrees
        )

        x = math.floor(lon / lon_grid_size)

        return x, y

    def _grid_coords_to_bounds(
        self, x: int, y: int
    ) -> tuple[float, float, float, float]:
        """
        Convert grid coordinates to geographic bounds.

        Parameters
        ----------
        x : int
            Grid X coordinate
        y : int
            Grid Y coordinate

        Returns
        -------
        tuple[float, float, float, float]
            Bounds as (min_lon, min_lat, max_lon, max_lat)
        """
        grid_size_degrees = 3.0 / 111320.0

        min_lat = y * grid_size_degrees
        max_lat = (y + 1) * grid_size_degrees

        # Adjust longitude grid size for latitude
        lat_center = (min_lat + max_lat) / 2
        lat_correction = math.cos(math.radians(lat_center))
        lon_grid_size = (
            grid_size_degrees / lat_correction
            if lat_correction != 0
            else grid_size_degrees
        )

        min_lon = x * lon_grid_size
        max_lon = (x + 1) * lon_grid_size

        return min_lon, min_lat, max_lon, max_lat

    def _generate_identifier(self, x: int, y: int) -> str:
        """
        Generate a unique identifier for the grid cell.

        Parameters
        ----------
        x : int
            Grid X coordinate
        y : int
            Grid Y coordinate

        Returns
        -------
        str
            Unique identifier for the cell
        """
        # Create a hash-based identifier that looks somewhat like What3Words format
        # This is NOT the actual What3Words algorithm
        combined = f"{x}_{y}"
        hash_obj = hashlib.md5(combined.encode())
        hash_hex = hash_obj.hexdigest()

        # Generate pseudo-words from hash
        word_parts = []
        for i in range(0, 6, 2):
            part = hash_hex[i : i + 2]
            # Convert to a word-like identifier
            word_parts.append(f"w{part}")

        return f"w3w.{'.'.join(word_parts[:3])}"

    @cached_method(cache_key_func=geo_cache_key)
    @override
    def get_cell_from_point(self, lat: float, lon: float) -> GridCell:
        """
        Get the grid cell containing the given point.

        Parameters
        ----------
        lat : float
            Latitude coordinate
        lon : float
            Longitude coordinate

        Returns
        -------
        GridCell
            The grid cell containing the specified point
        """
        x, y = self._lat_lon_to_grid_coords(lat, lon)
        min_lon, min_lat, max_lon, max_lat = self._grid_coords_to_bounds(x, y)
        epsilon = max(1e-12, (max_lat - min_lat) * 1e-6)
        min_lon -= epsilon
        min_lat -= epsilon
        max_lon += epsilon
        max_lat += epsilon

        polygon = Polygon(
            [
                (min_lon, min_lat),
                (max_lon, min_lat),
                (max_lon, max_lat),
                (min_lon, max_lat),
                (min_lon, min_lat),
            ]
        )

        identifier = self._generate_identifier(x, y)
        return GridCell(identifier, polygon, self.precision)

    @cached_method(cache_key_func=cell_cache_key)
    @override
    def get_cell_from_identifier(self, identifier: str) -> GridCell:
        """
        Get a grid cell from its identifier.

        Parameters
        ----------
        identifier : str
            The unique identifier for the grid cell

        Returns
        -------
        GridCell
            The grid cell corresponding to the identifier
        """
        if not identifier.startswith("w3w."):
            raise ValueError(f"Invalid What3Words identifier: {identifier}")

        # Provide a deterministic pseudo-cell without requiring the official API.
        hash_hex = hashlib.md5(identifier.encode()).hexdigest()
        x = int(hash_hex[:8], 16)
        y = int(hash_hex[8:16], 16)
        # Map hash-derived coordinates to a stable, bounded range.
        x = (x % 1_000_000) - 500_000
        y = (y % 1_000_000) - 500_000

        min_lon, min_lat, max_lon, max_lat = self._grid_coords_to_bounds(x, y)
        epsilon = max(1e-12, (max_lat - min_lat) * 1e-6)
        min_lon -= epsilon
        min_lat -= epsilon
        max_lon += epsilon
        max_lat += epsilon

        polygon = Polygon(
            [
                (min_lon, min_lat),
                (max_lon, min_lat),
                (max_lon, max_lat),
                (min_lon, max_lat),
                (min_lon, min_lat),
            ]
        )

        return GridCell(identifier, polygon, self.precision)

    @override
    def get_neighbors(self, cell: GridCell) -> list[GridCell]:
        """
        Get neighboring cells of the given cell.

        Parameters
        ----------
        cell : GridCell
            The cell for which to find neighbors

        Returns
        -------
        list[GridCell]
            List of neighboring grid cells (8 neighbors for square grid)
        """
        # Get center point of the cell
        centroid = cell.polygon.centroid
        lat, lon = centroid.y, centroid.x

        x, y = self._lat_lon_to_grid_coords(lat, lon)

        neighbors = []
        # Get 8 neighboring cells (Moore neighborhood)
        for dx in [-1, 0, 1]:
            for dy in [-1, 0, 1]:
                if dx == 0 and dy == 0:
                    continue  # Skip the center cell

                neighbor_x, neighbor_y = x + dx, y + dy
                min_lon, min_lat, max_lon, max_lat = self._grid_coords_to_bounds(
                    neighbor_x, neighbor_y
                )
                epsilon = max(1e-12, (max_lat - min_lat) * 1e-6)
                min_lon -= epsilon
                min_lat -= epsilon
                max_lon += epsilon
                max_lat += epsilon

                # Create neighbor cell
                polygon = Polygon(
                    [
                        (min_lon, min_lat),
                        (max_lon, min_lat),
                        (max_lon, max_lat),
                        (min_lon, max_lat),
                        (min_lon, min_lat),
                    ]
                )

                identifier = self._generate_identifier(neighbor_x, neighbor_y)
                neighbors.append(GridCell(identifier, polygon, self.precision))

        return neighbors

    @override
    def get_cells_in_bbox(
        self, min_lat: float, min_lon: float, max_lat: float, max_lon: float
    ) -> list[GridCell]:
        """
        Get all grid cells within the given bounding box.

        Parameters
        ----------
        min_lat : float
            Minimum latitude of bounding box
        min_lon : float
            Minimum longitude of bounding box
        max_lat : float
            Maximum latitude of bounding box
        max_lon : float
            Maximum longitude of bounding box

        Returns
        -------
        list[GridCell]
            List of grid cells within the bounding box
        """
        cells = []

        # Convert corners to grid coordinates
        x1, y1 = self._lat_lon_to_grid_coords(min_lat, min_lon)
        x2, y2 = self._lat_lon_to_grid_coords(max_lat, max_lon)

        # Ensure proper order
        min_x, max_x = min(x1, x2), max(x1, x2)
        min_y, max_y = min(y1, y2), max(y1, y2)

        # Generate cells in the bounding box
        for x in range(min_x, max_x + 1):
            for y in range(min_y, max_y + 1):
                cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat = (
                    self._grid_coords_to_bounds(x, y)
                )
                epsilon = max(1e-12, (cell_max_lat - cell_min_lat) * 1e-6)
                cell_min_lon -= epsilon
                cell_min_lat -= epsilon
                cell_max_lon += epsilon
                cell_max_lat += epsilon

                # Create cell polygon
                polygon = Polygon(
                    [
                        (cell_min_lon, cell_min_lat),
                        (cell_max_lon, cell_min_lat),
                        (cell_max_lon, cell_max_lat),
                        (cell_min_lon, cell_max_lat),
                        (cell_min_lon, cell_min_lat),
                    ]
                )

                identifier = self._generate_identifier(x, y)
                cells.append(GridCell(identifier, polygon, self.precision))

        return cells

    @override
    def _get_additional_columns(self, cell: GridCell) -> dict[str, Any]:
        """
        Add UTM zone column for What3Words cells.

        Parameters
        ----------
        cell : GridCell
            The grid cell to extract UTM data from

        Returns
        -------
        dict
            Dictionary with 'utm' column
        """
        if not cell.identifier or cell.polygon.is_empty:
            return {}

        centroid = cell.polygon.centroid
        utm_epsg = get_utm_epsg_code(centroid.y, centroid.x)
        return {"utm": utm_epsg}
