"""Shared HTTP session for API clients."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import httpx

from therminal._version import __version__
from therminal.config import TherminalConfig
from therminal.exceptions import (
    AuthenticationError,
    ForbiddenError,
    NotFoundError,
    RateLimitError,
    ServerError,
    TherminalError,
    ValidationError,
)


class HttpSession:
    """Shared HTTP session for API clients. Composition, not inheritance."""

    def __init__(self, config: TherminalConfig | None = None) -> None:
        self._config = config or TherminalConfig()
        self._client = httpx.Client(
            base_url=self._config.base_url,
            timeout=self._config.timeout,
            headers={"User-Agent": f"therminal-py/{__version__}"},
        )

    @property
    def config(self) -> TherminalConfig:
        """The resolved configuration for this session."""
        return self._config

    def close(self) -> None:
        """Close the underlying HTTP client."""
        self._client.close()

    def __enter__(self) -> HttpSession:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    # ── Request methods ──────────────────────────────────────

    def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
        """GET request, return parsed JSON."""
        cleaned = {k: v for k, v in (params or {}).items() if v is not None}
        resp = self._client.get(path, params=cleaned)
        _raise_for_status(resp)
        return resp.json()

    def get_all(
        self,
        path: str,
        params: dict[str, Any] | None = None,
        page_size: int = 10000,
        max_rows: int = 500000,
    ) -> list[dict[str, Any]]:
        """GET with auto-pagination. Fetches all pages until exhausted.

        The API caps JSON at 10K rows per page. This method transparently
        paginates to fetch the full result set (up to max_rows).
        If the caller passes a small limit (e.g., limit=50), that is respected.
        """
        cleaned = {k: v for k, v in (params or {}).items() if v is not None}

        # Respect caller's limit if smaller than page_size
        caller_limit = int(cleaned.get("limit", max_rows))
        effective_max = min(caller_limit, max_rows)
        cleaned["limit"] = min(page_size, effective_max)
        cleaned.setdefault("offset", 0)

        all_rows: list[dict[str, Any]] = []
        while len(all_rows) < effective_max:
            resp = self._client.get(path, params=cleaned)
            _raise_for_status(resp)
            page = resp.json()
            if not isinstance(page, list):
                break
            all_rows.extend(page)
            if len(page) < cleaned["limit"]:
                break  # Last page, no more data
            cleaned["offset"] += len(page)

        if len(all_rows) >= max_rows:
            import warnings

            warnings.warn(
                f"Result truncated at {max_rows} rows. Use format='parquet' for bulk downloads "
                f"(up to 500K rows without pagination).",
                stacklevel=3,
            )
        return all_rows

    def get_bytes(self, path: str, params: dict[str, Any] | None = None) -> bytes:
        """GET request, return raw bytes."""
        cleaned = {k: v for k, v in (params or {}).items() if v is not None}
        resp = self._client.get(path, params=cleaned)
        _raise_for_status(resp)
        return resp.content

    # ── Format routing ───────────────────────────────────────

    def handle_format(
        self,
        path: str,
        params: dict[str, Any],
        fmt: str | None,
        columns: list[str] | None,
        as_dataframe: bool,
        index_col: str | None,
        default_filename: str,
        save_path: str | Path | None,
    ) -> list[dict[str, Any]] | Path | Any:
        """Route format=json|csv|parquet.

        Returns:
            - list[dict] for JSON (default)
            - Path for csv/parquet (saved to disk)
            - DataFrame if as_dataframe=True (JSON or parquet)
        """
        if columns:
            params["columns"] = ",".join(columns)

        if fmt in ("csv", "parquet"):
            params["format"] = fmt
            content = self.get_bytes(path, params)

            if save_path is None:
                ext = "csv" if fmt == "csv" else "parquet"
                resolved_path = Path(f"{default_filename}.{ext}")
            else:
                resolved_path = Path(save_path)

            resolved_path.write_bytes(content)

            if as_dataframe and fmt == "parquet":
                try:
                    import pyarrow.parquet as pq
                except ImportError:
                    raise ImportError(
                        "pyarrow is required to load parquet files. "
                        "Install with: pip install therminal-py[pandas]"
                    ) from None
                df = pq.read_table(resolved_path).to_pandas()
                # Apply same datetime index as JSON path
                if index_col and index_col in df.columns:
                    import pandas as pd
                    df[index_col] = pd.to_datetime(
                        df[index_col].astype(str), utc=True
                    )
                    df = df.set_index(index_col).sort_index()
                return df

            if as_dataframe and fmt == "csv":
                try:
                    import pandas as pd
                except ImportError:
                    raise ImportError(
                        "pandas is required for DataFrame output. "
                        "Install with: pip install therminal-py[pandas]"
                    ) from None
                return pd.read_csv(resolved_path)

            return resolved_path

        # JSON path is handled by callers via get_all(), not handle_format().
        # If we reach here, it's a programming error.
        msg = f"handle_format called with fmt={fmt!r} — expected 'csv' or 'parquet'"
        raise ValueError(msg)


# ── Module-level helpers ─────────────────────────────────────


def _raise_for_status(resp: httpx.Response) -> None:
    """Map HTTP error responses to typed SDK exceptions."""
    if resp.is_success:
        return

    ct = resp.headers.get("content-type", "")
    body = resp.json() if ct.startswith("application/json") else {}
    msg = body.get("error", resp.text)

    if resp.status_code == 404:
        raise NotFoundError(msg)
    if resp.status_code == 429:
        retry = int(resp.headers.get("Retry-After", "1"))
        raise RateLimitError(retry_after=retry)
    if resp.status_code == 400:
        raise ValidationError(msg)
    if resp.status_code == 401:
        raise AuthenticationError(msg)
    if resp.status_code == 403:
        raise ForbiddenError(msg)
    if resp.status_code >= 500:
        raise ServerError(msg, status_code=resp.status_code)
    raise TherminalError(msg, status_code=resp.status_code)


def to_dataframe(data: list[dict[str, Any]], index_col: str | None = None) -> Any:
    """Convert list of dicts to a Pandas DataFrame."""
    try:
        import pandas as pd
    except ImportError:
        raise ImportError(
            "pandas is required for DataFrame output. "
            "Install it with: pip install therminal-py[pandas]"
        ) from None

    df = pd.DataFrame(data)
    if index_col and index_col in df.columns:
        df[index_col] = pd.to_datetime(df[index_col], utc=True)
        df = df.set_index(index_col).sort_index()
    return df


def normalize_date(date_str: str | None) -> str | None:
    """Normalize date strings -- append T00:00:00Z if only YYYY-MM-DD."""
    if date_str is None:
        return None
    if len(date_str) == 10 and "T" not in date_str:
        return f"{date_str}T00:00:00Z"
    return date_str
