"""
Map-matching Orchestrator.

Manages the chunked, out-of-core streaming of GPS trajectories through
the routing engine backend and immediately writes snapped records to disk.
"""

import logging
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple, Union

from ..ingest import write_walks
from .gps import GPSDataset
from .base import BaseMatcher

if TYPE_CHECKING:
    from ...core.network import Network
    from ...core.walk import WalkFrame

logger = logging.getLogger(__name__)


def match_to_disk(
    dataset: GPSDataset,
    backend: BaseMatcher,
    network: "Network",
    output_dir: Union[str, Path],
    chunk_size_trips: int = 1000,
    node_attributes: Optional[List[str]] = None,
    edge_attributes: Optional[List[str]] = None,
) -> "WalkFrame":
    """Stream GPS data through the matcher and write Parquet incrementally.

    Args:
        dataset: Out-of-core GPS dataset.
        backend: A :class:`BaseMatcher` implementation.
        network: The walkingpandas Network.
        output_dir: Directory for output Parquet chunks.
        chunk_size_trips: Number of trips per processing chunk.
        node_attributes: Attributes to project onto nodes.
        edge_attributes: Attributes to project onto edges.

    Returns:
        A :class:`WalkFrame` over the written Parquet files.

    Raises:
        ValueError: If no trajectories could be matched.
    """
    from ...core.walk import WalkFrame

    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    all_trips = dataset.get_unique_trip_ids()
    chunk_index = 0

    for i in range(0, len(all_trips), chunk_size_trips):
        trip_chunk = all_trips[i : i + chunk_size_trips]

        chunk_df = dataset.load_trips(trip_chunk)
        if chunk_df.empty:
            continue

        raw_traces = backend.trace(chunk_df)

        snapped_df = backend.snap(raw_traces, network)

        enriched_df = backend.enrich(
            snapped_df,
            raw_traces,
            node_attributes=node_attributes,
            edge_attributes=edge_attributes,
        )

        if not enriched_df.empty:
            out_file = out_dir / f"chunk_{chunk_index:06d}.parquet"
            write_walks(enriched_df, out_file, schema_validate=False)
            chunk_index += 1

    if chunk_index == 0:
        raise ValueError("No trajectories were successfully matched.")

    return WalkFrame(str(out_dir / "*.parquet"), network=network)


def match_trajectories(
    gps_data: Union[str, Path, List[Union[str, Path]]],
    backend: BaseMatcher,
    output_dir: Union[str, Path],
    id_col: str = "trip_id",
    time_col: str = "timestamp",
    geom_cols: Tuple[str, str] = ("lon", "lat"),
    node_attributes: Optional[List[str]] = None,
    edge_attributes: Optional[List[str]] = None,
    chunk_size_trips: int = 1000,
    network: Optional["Network"] = None,
) -> "WalkFrame":
    """High-level API entry point for map-matching.

    Creates a lazy dataset, estimates boundaries, synchronises the routing
    engine, and returns a queryable WalkFrame.

    Args:
        gps_data: Path(s) to CSV / Parquet GPS files.
        backend: A :class:`BaseMatcher` implementation.
        output_dir: Where to write matched Parquet chunks.
        id_col: Column identifying individual trips.
        time_col: Timestamp column.
        geom_cols: ``(lon_col, lat_col)`` pair.
        node_attributes: Attributes to project onto nodes.
        edge_attributes: Attributes to project onto edges.
        chunk_size_trips: Trips per processing chunk.
        network: Optional pre-built Network (auto-derived if ``None``).

    Returns:
        A :class:`WalkFrame`.
    """
    keep_attrs = list(set((node_attributes or []) + (edge_attributes or [])))

    dataset = GPSDataset(
        file_pattern=gps_data,
        id_col=id_col,
        time_col=time_col,
        lon_col=geom_cols[0],
        lat_col=geom_cols[1],
        keep_attributes=keep_attrs,
    )

    if getattr(backend, "region", None) is None and hasattr(backend, "set_region"):
        backend.set_region(dataset.estimate_bbox())

    if network is None:
        network = backend.get_network()

    return match_to_disk(
        dataset=dataset,
        backend=backend,
        network=network,
        output_dir=output_dir,
        chunk_size_trips=chunk_size_trips,
        node_attributes=node_attributes,
        edge_attributes=edge_attributes,
    )
