"""
Spatial filtering utilities for WalkFrame.

Uses DuckDB subqueries for spatial predicates so that ID sets never
need to be materialised in Python — the database engine handles the
join between the walk view and the network tables directly.
"""

from typing import TYPE_CHECKING, Union, List, Tuple

from .._utils import _format_id_list_sql

try:
    from shapely.geometry import Point, Polygon, LineString
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 a DuckDB subquery.

    The spatial predicate runs entirely inside DuckDB — no IDs are
    materialised in Python.

    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
    subquery = (
        f"SELECT node_id FROM wp_nodes "
        f"WHERE ST_Within(geom, ST_MakeEnvelope({minx}, {miny}, {maxx}, {maxy}))"
    )
    walkframe._query_state['filters'].append(f"node_id IN ({subquery})")
    return walkframe


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

    The spatial predicate runs entirely inside DuckDB — no IDs are
    materialised in Python.

    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
    node_sub = (
        f"SELECT node_id FROM wp_nodes "
        f"WHERE ST_Intersects(geom, ST_GeomFromText('{geom_wkt}'))"
    )
    edge_sub = (
        f"SELECT edge_id FROM wp_edges "
        f"WHERE ST_Intersects(geom, ST_GeomFromText('{geom_wkt}'))"
    )
    walkframe._query_state['filters'].append(
        f"(node_id IN ({node_sub}) OR exit_edge IN ({edge_sub}))"
    )
    return walkframe


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

    Uses DuckDB's ``ST_DWithin`` so the distance calculation happens
    inside the database engine.

    Args:
        walkframe: WalkFrame instance to filter.
        point: Shapely Point geometry.
        distance: Distance threshold **in CRS units** (degrees for
            EPSG:4326, metres for projected CRS).

    Returns:
        WalkFrame: Filtered WalkFrame (method chaining).

    Examples:
        >>> from shapely.geometry import Point
        >>> pt = Point(0.5, 0.5)
        >>> walks = WalkFrame("data/walks/*.parquet", network=network)
        >>> filtered = walks.near_point(pt, distance=0.01).compute()
    """
    lon, lat = point.x, point.y
    node_sub = (
        f"SELECT node_id FROM wp_nodes "
        f"WHERE ST_DWithin(geom, ST_Point({lon}, {lat}), {distance})"
    )
    edge_sub = (
        f"SELECT edge_id FROM wp_edges "
        f"WHERE ST_DWithin(geom, ST_Point({lon}, {lat}), {distance})"
    )
    walkframe._query_state['filters'].append(
        f"(node_id IN ({node_sub}) OR exit_edge IN ({edge_sub}))"
    )
    return walkframe


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
    if not getattr(net, 'has_string_mapping', False):
        return ids

    conn = net.connection
    resolved = []
    for v in ids:
        if isinstance(v, str):
            row = conn.execute(
                "SELECT int_id FROM wp_node_map WHERE original_id = ?", [v]
            ).fetchone()
            resolved.append(row[0] if row else v)
        else:
            resolved.append(v)
    return resolved
