"""
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, Optional, Tuple, Union

import duckdb
import pandas as pd

from walkingpandas._utils import BoundingBox

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 walks.
        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 = "walk_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, buffer: Optional[float] = None
    ) -> BoundingBox:
        """Fast, out-of-core bounding box estimation.

        Args:
            buffer: If given, extend every side of the bounding box
                by this many **metres**.  The conversion uses a
                latitude-dependent approximation for the east/west
                expansion and a fixed ~111 320 m/degree for north/south.

        Returns:
            A :class:`~walkingpandas.BoundingBox` covering all observations
            (optionally expanded by *buffer*).
        """
        import math

        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 BoundingBox(north=0.0, south=0.0, east=0.0, west=0.0)

        north, south, east, west = (
            float(res[0]), float(res[1]), float(res[2]), float(res[3]),
        )

        if buffer is not None:
            _M_PER_DEG_LAT = 111_320.0
            lat_mid = math.radians((north + south) / 2.0)
            m_per_deg_lon = _M_PER_DEG_LAT * math.cos(lat_mid)

            dlat = buffer / _M_PER_DEG_LAT
            dlon = buffer / m_per_deg_lon if m_per_deg_lon > 0 else dlat

            north += dlat
            south -= dlat
            east += dlon
            west -= dlon

        return BoundingBox(north=north, south=south, east=east, west=west)

    def get_unique_walk_ids(self) -> List:
        """Return all distinct walk 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_walks(self, walk_ids: Optional[List] = None) -> pd.DataFrame:
        """Load walks into memory with standardised column names.

        Args:
            walk_ids: Which walks to materialise. If ``None`` or empty,
                loads all walks in the dataset.

        Returns:
            DataFrame with columns ``walk_id``, ``timestamp``, ``lon``,
            ``lat`` plus any *keep_attributes*.
        """
        selects = [
            f'"{self.id_col}" AS walk_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)
        order_clause = f'ORDER BY "{self.id_col}", "{self.time_col}"'

        if walk_ids is None:
            query = f"SELECT {cols_str} FROM raw_gps {order_clause}"
            return self.conn.execute(query).df()

        if not walk_ids:
            return pd.DataFrame()

        placeholders = ",".join(["?"] * len(walk_ids))
        query = (
            f"SELECT {cols_str} FROM raw_gps "
            f'WHERE "{self.id_col}" IN ({placeholders}) {order_clause}'
        )
        return self.conn.execute(query, walk_ids).df()
