"""CSV source adapter — read CSV files using pandas.

Handles encoding, delimiter, quoting, and multi-value pipe/comma encoding
commonly found in CEDS data exports.
"""

from __future__ import annotations

from collections.abc import Iterator
from pathlib import Path
from typing import Any

import pandas as pd

from ceds_jsonld.adapters.base import SourceAdapter
from ceds_jsonld.exceptions import AdapterError


class CSVAdapter(SourceAdapter):
    """Read records from a CSV file.

    Example:
        >>> adapter = CSVAdapter("students.csv")
        >>> for row in adapter.read():
        ...     print(row["FirstName"])
    """

    def __init__(
        self,
        path: str | Path,
        *,
        encoding: str = "utf-8",
        delimiter: str = ",",
        dtype: type | dict[str, type] | None = str,
        na_values: list[str] | None = None,
        **pandas_kwargs: Any,
    ) -> None:
        """Initialize with a file path and optional pandas read_csv options.

        Args:
            path: Path to the CSV file.
            encoding: File encoding (default: utf-8).
            delimiter: Column delimiter (default: comma).
            dtype: Column dtypes. Defaults to ``str`` to avoid pandas
                auto-converting identifiers to float.
            na_values: Additional strings to treat as NA / missing.
            **pandas_kwargs: Any additional keyword arguments forwarded to
                ``pandas.read_csv()``.
        """
        self._path = Path(path)
        if not self._path.exists():
            msg = f"CSV file not found: {self._path}"
            raise AdapterError(msg)
        self._encoding = encoding
        self._delimiter = delimiter
        self._dtype = dtype
        self._na_values = na_values
        self._pandas_kwargs = pandas_kwargs

    def read(self, **kwargs: Any) -> Iterator[dict[str, Any]]:
        """Yield each CSV row as a dict.

        Missing / NaN values are converted to empty strings.

        Returns:
            Iterator of dicts keyed by column header.
        """
        try:
            df = pd.read_csv(
                self._path,
                encoding=self._encoding,
                delimiter=self._delimiter,
                dtype=self._dtype,
                na_values=self._na_values,
                keep_default_na=True,
                **self._pandas_kwargs,
            )
        except Exception as exc:
            msg = f"Failed to read CSV '{self._path}': {exc}"
            raise AdapterError(msg) from exc

        # Replace NaN with empty string for downstream mapper compatibility
        df = df.fillna("")

        yield from df.to_dict(orient="records")

    def read_batch(self, batch_size: int = 1000, **kwargs: Any) -> Iterator[list[dict[str, Any]]]:
        """Yield batches by reading with pandas chunksize.

        Args:
            batch_size: Number of rows per chunk.

        Returns:
            Iterator of lists of dicts.
        """
        try:
            reader = pd.read_csv(
                self._path,
                encoding=self._encoding,
                delimiter=self._delimiter,
                dtype=self._dtype,
                na_values=self._na_values,
                keep_default_na=True,
                chunksize=batch_size,
                **self._pandas_kwargs,
            )
        except Exception as exc:
            msg = f"Failed to read CSV '{self._path}': {exc}"
            raise AdapterError(msg) from exc

        for chunk in reader:
            chunk = chunk.fillna("")
            yield chunk.to_dict(orient="records")

    def count(self) -> int | None:
        """Return the number of data rows in the CSV (excludes header).

        Blank / whitespace-only lines are not counted, matching the
        behaviour of :meth:`read` (which delegates to pandas and skips
        them).  An empty file returns ``0``.
        """
        try:
            with self._path.open(encoding=self._encoding) as f:
                header_seen = False
                data_rows = 0
                for line in f:
                    if not line.strip():
                        continue
                    if not header_seen:
                        header_seen = True  # skip header
                        continue
                    data_rows += 1
                return data_rows
        except OSError:
            return None
