"""Therminal API client."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import httpx

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

DEFAULT_BASE_URL = "https://api.mostlyright.xyz"
DEFAULT_TIMEOUT = 30.0


class TherminalClient:
    """Client for the Therminal REST API.

    Args:
        base_url: API base URL. Defaults to https://api.mostlyright.xyz
        timeout: Request timeout in seconds. Defaults to 30.
    """

    def __init__(
        self,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
    ):
        self._client = httpx.Client(
            base_url=base_url,
            timeout=timeout,
            headers={"User-Agent": f"therminal-py/{__version__}"},
        )

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

    def __enter__(self) -> TherminalClient:
        return self

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

    # ── Internal ─────────────────────────────────────────────

    def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
        """Make a GET request and 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)
        self._raise_for_status(resp)
        return resp.json()

    def _get_bytes(self, path: str, params: dict[str, Any] | None = None) -> bytes:
        """Make a GET request and 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)
        self._raise_for_status(resp)
        return resp.content

    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:
        """Handle format=json|csv|parquet routing.

        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"
                save_path = Path(f"{default_filename}.{ext}")
            else:
                save_path = Path(save_path)

            save_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
                return pq.read_table(save_path).to_pandas()

            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(save_path)

            return save_path

        # Default: JSON
        data = self._get(path, params)
        return self._to_dataframe(data, index_col=index_col) if as_dataframe else data

    @staticmethod
    def _raise_for_status(resp: httpx.Response) -> None:
        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)

    @staticmethod
    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])
            df = df.set_index(index_col)
        return df

    # ── Health ───────────────────────────────────────────────

    def health(self) -> dict[str, Any]:
        """Check API health status."""
        return self._get("/health")

    # ── Series ───────────────────────────────────────────────

    def series(
        self,
        active: bool | None = None,
        limit: int = 100,
        offset: int = 0,
        as_dataframe: bool = False,
    ) -> list[dict[str, Any]] | Any:
        """List event series.

        Args:
            active: Filter by active status.
            limit: Max results (default 100).
            offset: Pagination offset.
            as_dataframe: Return as Pandas DataFrame.
        """
        params = {
            "active": str(active).lower() if active is not None else None,
            "limit": limit,
            "offset": offset,
        }
        data = self._get("/api/v1/series", params)
        return self._to_dataframe(data) if as_dataframe else data

    def series_detail(self, ticker: str) -> dict[str, Any]:
        """Get a single series by ticker."""
        return self._get(f"/api/v1/series/{ticker}")

    # ── Markets ──────────────────────────────────────────────

    def markets(
        self,
        series: str | None = None,
        station: str | None = None,
        from_date: str | None = None,
        to_date: str | None = None,
        limit: int = 1000,
        offset: int = 0,
        as_dataframe: bool = False,
    ) -> list[dict[str, Any]] | Any:
        """List markets.

        Args:
            series: Filter by series ticker.
            station: Filter by station code.
            from_date: Filter by close_time >= (RFC3339 or YYYY-MM-DD).
            to_date: Filter by close_time <= (RFC3339 or YYYY-MM-DD).
            as_dataframe: Return as Pandas DataFrame.
        """
        params = {
            "series": series,
            "station": station,
            "from": self._normalize_date(from_date),
            "to": self._normalize_date(to_date),
            "limit": limit,
            "offset": offset,
        }
        data = self._get("/api/v1/markets", params)
        return self._to_dataframe(data) if as_dataframe else data

    def market(self, ticker: str) -> dict[str, Any]:
        """Get a single market by ticker."""
        return self._get(f"/api/v1/markets/{ticker}")

    # ── Candles ──────────────────────────────────────────────

    def candles(
        self,
        market: str | None = None,
        event: str | None = None,
        station: str | None = None,
        series: str | None = None,
        from_date: str | None = None,
        to_date: str | None = None,
        interval: int | None = None,
        fill: bool = True,
        aggregate: int | None = None,
        tz: str | None = None,
        format: str | None = None,
        columns: list[str] | None = None,
        limit: int = 10000,
        offset: int = 0,
        as_dataframe: bool = False,
        save_path: str | Path | None = None,
    ) -> list[dict[str, Any]] | Path | Any:
        """Get OHLCV candles for prediction markets.

        Args:
            market: Market ticker (e.g., KXHIGHNY-26MAR20-T50).
            event: Event ticker.
            station: Station code (e.g., NYC).
            series: Series ticker.
            from_date: Start date (RFC3339 or YYYY-MM-DD).
            to_date: End date (RFC3339 or YYYY-MM-DD).
            interval: Candle interval in minutes (1, 5, 15, 30, 60, 1440).
            fill: Forward-fill gaps (default True).
            aggregate: Aggregate to larger interval (minutes).
            tz: Timezone (UTC, station, or IANA name).
            format: Output format: None/json (default), csv, parquet.
            columns: List of columns to include (e.g., ["close", "volume"]).
            as_dataframe: Return as Pandas DataFrame.
            save_path: File path for csv/parquet output.
        """
        params: dict[str, Any] = {
            "market": market,
            "event": event,
            "station": station,
            "series": series,
            "from": self._normalize_date(from_date),
            "to": self._normalize_date(to_date),
            "interval": interval,
            "fill": "false" if not fill else None,
            "aggregate": aggregate,
            "tz": tz,
            "limit": limit,
            "offset": offset,
        }
        return self._handle_format(
            "/api/v1/candles", params, format, columns,
            as_dataframe, "end_period_ts", "candles", save_path,
        )

    # ── Observations ─────────────────────────────────────────

    def observations(
        self,
        station: str | None = None,
        from_date: str | None = None,
        to_date: str | None = None,
        obs_type: str | None = None,
        units: str | None = None,
        tz: str | None = None,
        format: str | None = None,
        columns: list[str] | None = None,
        limit: int = 1000,
        offset: int = 0,
        as_dataframe: bool = False,
        save_path: str | Path | None = None,
    ) -> list[dict[str, Any]] | Path | Any:
        """Get weather observations (METAR/SPECI).

        Args:
            station: Station code (e.g., NYC, LAX).
            from_date: Start date (RFC3339).
            to_date: End date (RFC3339).
            obs_type: Filter by type (METAR, SPECI).
            units: Unit system (raw, metric, imperial).
            tz: Timezone (UTC, station, or IANA name).
            format: Output format: None/json (default), csv, parquet.
            columns: List of columns to include.
            as_dataframe: Return as Pandas DataFrame.
            save_path: File path for csv/parquet output.
        """
        params: dict[str, Any] = {
            "station": station,
            "from": self._normalize_date(from_date),
            "to": self._normalize_date(to_date),
            "type": obs_type,
            "units": units,
            "tz": tz,
            "limit": limit,
            "offset": offset,
        }
        return self._handle_format(
            "/api/v1/observations", params, format, columns,
            as_dataframe, "observed_at", "observations", save_path,
        )

    # ── Climate ──────────────────────────────────────────────

    def climate(
        self,
        station: str | None = None,
        from_date: str | None = None,
        to_date: str | None = None,
        units: str | None = None,
        format: str | None = None,
        columns: list[str] | None = None,
        limit: int = 1000,
        offset: int = 0,
        as_dataframe: bool = False,
        save_path: str | Path | None = None,
    ) -> list[dict[str, Any]] | Path | Any:
        """Get daily climate reports (NWS CLI products).

        Args:
            station: Station code (e.g., NYC).
            from_date: Start date (YYYY-MM-DD).
            to_date: End date (YYYY-MM-DD).
            units: Unit system (raw, metric, imperial).
            format: Output format: None/json (default), csv, parquet.
            columns: List of columns to include.
            as_dataframe: Return as Pandas DataFrame.
            save_path: File path for csv/parquet output.
        """
        params: dict[str, Any] = {
            "station": station,
            "from": from_date,
            "to": to_date,
            "units": units,
            "limit": limit,
            "offset": offset,
        }
        return self._handle_format(
            "/api/v1/climate", params, format, columns,
            as_dataframe, "observation_date", "climate", save_path,
        )

    # ── Analysis ─────────────────────────────────────────────

    def climate_gaps(
        self,
        station: str,
        from_date: str | None = None,
        to_date: str | None = None,
        as_dataframe: bool = False,
    ) -> list[dict[str, Any]] | Any:
        """Find gaps in climate report history for a station.

        Args:
            station: Station code (required).
            from_date: Start date (YYYY-MM-DD). Defaults to 5 years ago.
            to_date: End date (YYYY-MM-DD). Defaults to today.
            as_dataframe: Return as Pandas DataFrame.
        """
        params = {
            "station": station,
            "from": from_date,
            "to": to_date,
        }
        data = self._get("/api/v1/analysis/climate-gaps", params)
        return self._to_dataframe(data) if as_dataframe else data

    # ── Helpers ───────────────────────────────────────────────

    @staticmethod
    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
