"""
Spatial filtering utilities for WalkFrame with optimization.

Uses reverse spatial filtering: queries the small Network first to get
node/edge IDs, then applies fast integer lookups on Parquet data.
"""

from typing import TYPE_CHECKING, Union, List, Tuple
import geopandas as gpd

from .._utils import _format_id_list_sql

try:
    from shapely.geometry import Point, Polygon, LineString, box
except ImportError:
    raise ImportError("shapely is required for spatial filtering. Install with: pip install shapely")

if TYPE_CHECKING:
    from ..core.walk import WalkFrame


def passing_through(walkframe: 'WalkFrame', nodes: Union[int, str, List]) -> 'WalkFrame':
    """Filter walks that pass through specified node(s).

    Args:
        walkframe: WalkFrame instance to filter.
        nodes: Node ID(s) to filter by. Integers are used directly;
            strings are resolved through the network's translation table
            when available.

    Returns:
        WalkFrame: Filtered WalkFrame (method chaining).

    Examples:
        >>> walks = WalkFrame("data/walks/*.parquet", network=network)
        >>> filtered = walks.passing_through([0, 1]).compute()
    """
    if isinstance(nodes, (int, str)):
        nodes = [nodes]

    resolved = _resolve_node_ids(walkframe, nodes)
    nodes_sql = _format_id_list_sql(resolved)
    walkframe._query_state['filters'].append(f"node_id IN ({nodes_sql})")

    return walkframe


def within_bounds(walkframe: 'WalkFrame', bbox: Tuple[float, float, float, float]) -> 'WalkFrame':
    """Filter walks within a bounding box using reverse spatial filtering.

    Queries the Network first to get node_ids within the bounding box,
    then applies fast integer lookup on Parquet data.

    Args:
        walkframe: WalkFrame instance to filter.
        bbox: ``(minx, miny, maxx, maxy)`` bounding box.

    Returns:
        WalkFrame: Filtered WalkFrame (method chaining).

    Examples:
        >>> walks = WalkFrame("data/walks/*.parquet", network=network)
        >>> filtered = walks.within_bounds((-122.5, 37.7, -122.3, 37.8)).compute()
    """
    minx, miny, maxx, maxy = bbox

    network_conn = walkframe._network.connection
    node_query = f"""
        SELECT node_id
        FROM wp_nodes
        WHERE ST_Within(geom, ST_MakeEnvelope({minx}, {miny}, {maxx}, {maxy}))
    """

    try:
        node_ids_df = network_conn.execute(node_query).df()
        if node_ids_df.empty:
            walkframe._query_state['filters'].append("1=0")
            return walkframe

        node_ids = node_ids_df['node_id'].tolist()
        nodes_sql = _format_id_list_sql(node_ids)
        walkframe._query_state['filters'].append(f"node_id IN ({nodes_sql})")
    except Exception as e:
        import warnings
        warnings.warn(f"Spatial optimization failed, using fallback: {e}")

    return walkframe


def intersecting(walkframe: 'WalkFrame', geometry: Union[Polygon, LineString, Point]) -> 'WalkFrame':
    """Filter walks intersecting a geometry using reverse spatial filtering.

    Queries the Network first to get node_ids/edge_ids intersecting
    the geometry, then applies fast integer lookup on Parquet data.

    Args:
        walkframe: WalkFrame instance to filter.
        geometry: Shapely geometry to intersect with.

    Returns:
        WalkFrame: Filtered WalkFrame (method chaining).

    Examples:
        >>> from shapely.geometry import Polygon
        >>> poly = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
        >>> walks = WalkFrame("data/walks/*.parquet", network=network)
        >>> filtered = walks.intersecting(poly).compute()
    """
    geom_wkt = geometry.wkt
    network_conn = walkframe._network.connection

    node_query = f"""
        SELECT node_id
        FROM wp_nodes
        WHERE ST_Intersects(geom, ST_GeomFromText('{geom_wkt}'))
    """
    edge_query = f"""
        SELECT edge_id
        FROM wp_edges
        WHERE ST_Intersects(geom, ST_GeomFromText('{geom_wkt}'))
    """

    try:
        node_ids_df = network_conn.execute(node_query).df()
        edge_ids_df = network_conn.execute(edge_query).df()

        filters = []

        if not node_ids_df.empty:
            node_ids = node_ids_df['node_id'].tolist()
            nodes_sql = _format_id_list_sql(node_ids)
            filters.append(f"node_id IN ({nodes_sql})")

        if not edge_ids_df.empty:
            edge_ids = edge_ids_df['edge_id'].tolist()
            edges_sql = _format_id_list_sql(edge_ids)
            filters.append(f"exit_edge IN ({edges_sql})")

        if filters:
            walkframe._query_state['filters'].append(f"({' OR '.join(filters)})")
        else:
            walkframe._query_state['filters'].append("1=0")
    except Exception as e:
        import warnings
        warnings.warn(f"Spatial optimization failed: {e}")
        walkframe._query_state['filters'].append("1=0")

    return walkframe


def near_point(walkframe: 'WalkFrame', point: Point, distance: float) -> 'WalkFrame':
    """Filter walks near a point within specified distance.

    Creates a buffer around the point and delegates to ``intersecting``.

    Args:
        walkframe: WalkFrame instance to filter.
        point: Shapely Point geometry.
        distance: Distance threshold (in same units as geometry CRS).

    Returns:
        WalkFrame: Filtered WalkFrame (method chaining).

    Examples:
        >>> from shapely.geometry import Point
        >>> pt = Point(-122.4, 37.8)
        >>> walks = WalkFrame("data/walks/*.parquet", network=network)
        >>> filtered = walks.near_point(pt, distance=1000).compute()
    """
    buffer = point.buffer(distance)
    return intersecting(walkframe, buffer)


def _resolve_node_ids(walkframe: 'WalkFrame', ids: list) -> list:
    """Translate user-provided node IDs through the network's string mapping.

    If the network has a ``wp_node_map`` and any *ids* are strings, they are
    resolved to integer IDs.  Otherwise the values are returned as-is.
    """
    net = walkframe._network
    has_map = getattr(net, 'has_string_mapping', False)
    if not has_map:
        return ids

    resolved = []
    for v in ids:
        if isinstance(v, str):
            resolved.append(net._node_original_to_int.get(v, v))
        else:
            resolved.append(v)
    return resolved
