"""
Lazy-evaluation dataset manager for raw GPS trajectories.

Uses DuckDB to scan and query massive CSV or Parquet files out-of-core,
allowing for efficient bounding box estimation and trip chunking without
loading the entire dataset into RAM.
"""

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

import duckdb
import pandas as pd

logger = logging.getLogger(__name__)


class GPSDataset:
    """Out-of-core accessor for raw GPS trajectory files.

    Args:
        file_pattern: One or more CSV / Parquet file paths (or glob patterns).
        id_col: Column that identifies individual trips.
        time_col: Timestamp column.
        lon_col: Longitude column.
        lat_col: Latitude column.
        keep_attributes: Extra columns to preserve through the pipeline.

    Raises:
        ValueError: If *file_pattern* is empty.
        duckdb.IOException: If the file(s) cannot be read.
    """

    def __init__(
        self,
        file_pattern: Union[str, Path, List[Union[str, Path]]],
        id_col: str = "trip_id",
        time_col: str = "timestamp",
        lon_col: str = "lon",
        lat_col: str = "lat",
        keep_attributes: List[str] | None = None,
    ):
        self.id_col = id_col
        self.time_col = time_col
        self.lon_col = lon_col
        self.lat_col = lat_col
        self.keep_attributes = keep_attributes or []

        if isinstance(file_pattern, (str, Path)):
            self.paths = [str(file_pattern)]
        else:
            self.paths = [str(p) for p in file_pattern]

        if not self.paths:
            raise ValueError("file_pattern must contain at least one path.")

        self.conn = duckdb.connect(":memory:")

        ext = Path(self.paths[0]).suffix.lower()
        reader = "read_parquet" if ".parquet" in ext else "read_csv_auto"

        escaped = ", ".join(f"'{p}'" for p in self.paths)
        self.conn.execute(
            f"CREATE VIEW raw_gps AS SELECT * FROM {reader}([{escaped}])"
        )

    def estimate_bbox(self) -> Tuple[float, float, float, float]:
        """Fast, out-of-core bounding box estimation.

        Returns:
            ``(north, south, east, west)`` tuple.
        """
        query = (
            f'SELECT MAX("{self.lat_col}"), MIN("{self.lat_col}"), '
            f'MAX("{self.lon_col}"), MIN("{self.lon_col}") FROM raw_gps'
        )
        res = self.conn.execute(query).fetchone()

        if res[0] is None:
            return (0.0, 0.0, 0.0, 0.0)

        return (float(res[0]), float(res[1]), float(res[2]), float(res[3]))

    def get_unique_trip_ids(self) -> List:
        """Return all distinct trip IDs (out-of-core)."""
        query = f'SELECT DISTINCT "{self.id_col}" FROM raw_gps ORDER BY 1'
        return [row[0] for row in self.conn.execute(query).fetchall()]

    def load_trips(self, trip_ids: List) -> pd.DataFrame:
        """Load a chunk of trips into memory with standardised column names.

        Args:
            trip_ids: Which trips to materialise.

        Returns:
            DataFrame with columns ``trip_id``, ``timestamp``, ``lon``,
            ``lat`` plus any *keep_attributes*.
        """
        if not trip_ids:
            return pd.DataFrame()

        placeholders = ",".join(["?"] * len(trip_ids))

        selects = [
            f'"{self.id_col}" AS trip_id',
            f'"{self.time_col}" AS timestamp',
            f'"{self.lon_col}" AS lon',
            f'"{self.lat_col}" AS lat',
        ]

        for attr in self.keep_attributes:
            selects.append(f'"{attr}"')

        cols_str = ", ".join(selects)
        query = (
            f"SELECT {cols_str} FROM raw_gps "
            f'WHERE "{self.id_col}" IN ({placeholders}) '
            f'ORDER BY "{self.id_col}", "{self.time_col}"'
        )

        return self.conn.execute(query, trip_ids).df()
