from __future__ import annotations

from collections.abc import Mapping
from datetime import datetime
from pathlib import Path
from typing import Any, List

import httpx
from astropy.table import Row, Table

from .conf import conf
from .exceptions import AuthenticationError, Grad300APIError, Grad300Error
from .models import ScanRecord, ScanType
from .scans import DownloadResult, ScansService


class Grad300Client:
    """Small Python client for the GRAD300 backend API."""

    def __init__(
        self,
        base_url: str | None = None,
        *,
        api_prefix: str | None = None,
        timeout: float | None = None,
        token: str | None = None,
        user: str | None = None,
        password: str | None = None,
        transport: httpx.BaseTransport | None = None,
    ) -> None:
        if token and (user is not None or password is not None):
            raise ValueError("Use either 'token' or both 'user'/'password', not both.")

        if (user is None) != (password is None):
            raise ValueError("Both 'user' and 'password' must be provided together.")

        resolved_base_url = base_url or str(conf.base_url)
        resolved_api_prefix = api_prefix or str(conf.api_prefix)
        resolved_timeout = float(conf.timeout) if timeout is None else timeout

        self._default_page_size = int(conf.default_page_size)
        self._base_api_url = _build_api_base_url(
            base_url=resolved_base_url,
            api_prefix=resolved_api_prefix,
        )
        self._client = httpx.Client(
            base_url=self._base_api_url,
            timeout=resolved_timeout,
            transport=transport,
            headers={"Accept": "application/json"},
        )
        self._scans = ScansService(self)

        if token:
            self.set_token(token)
        elif user is not None and password is not None:
            self.login(user, password)

    @property
    def base_api_url(self) -> str:
        return self._base_api_url

    def set_token(self, token: str) -> None:
        self._client.headers["Authorization"] = f"Bearer {token}"

    def clear_token(self) -> None:
        self._client.headers.pop("Authorization", None)

    def login(self, email: str, password: str) -> str:
        response = self._request(
            "POST",
            "/login/access-token",
            data={"username": email, "password": password},
        )
        payload = response.json()
        token = payload.get("access_token")
        if not isinstance(token, str) or not token:
            raise Grad300Error("Login response did not include an access token")

        self.set_token(token)
        return token

    def test_token(self) -> dict[str, Any]:
        payload = self.get_json("/login/test-token")
        if not isinstance(payload, dict):
            raise ValueError("Unexpected payload from /login/test-token")
        return payload

    def list(
        self,
        scan_type: ScanType | str,
        *,
        offset: int = 0,
        limit: int | None = None,
        source: str | None = None,
        scan_name: str | None = None,
        date_from: datetime | None = None,
        date_to: datetime | None = None,
    ) -> Table:
        resolved_limit = self._resolve_page_size(limit, field_name="limit")
        return self._scans.list(
            scan_type,
            offset=offset,
            limit=resolved_limit,
            source=source,
            scan_name=scan_name,
            date_from=date_from,
            date_to=date_to,
        )

    def list_records(
        self,
        scan_type: ScanType | str,
        *,
        offset: int = 0,
        limit: int | None = None,
        source: str | None = None,
        scan_name: str | None = None,
        date_from: datetime | None = None,
        date_to: datetime | None = None,
    ) -> List[ScanRecord]:
        resolved_limit = self._resolve_page_size(limit, field_name="limit")
        return self._scans.list_records(
            scan_type,
            offset=offset,
            limit=resolved_limit,
            source=source,
            scan_name=scan_name,
            date_from=date_from,
            date_to=date_to,
        )

    def query(
        self,
        *,
        scan_type: ScanType | str | None = None,
        source: str | None = None,
        scan_name: str | None = None,
        date_from: datetime | None = None,
        date_to: datetime | None = None,
        page_size: int | None = None,
        max_results: int | None = None,
    ) -> Table:
        resolved_page_size = self._resolve_page_size(page_size, field_name="page_size")
        return self._scans.query(
            scan_type=scan_type,
            source=source,
            scan_name=scan_name,
            date_from=date_from,
            date_to=date_to,
            page_size=resolved_page_size,
            max_results=max_results,
        )

    def query_records(
        self,
        *,
        scan_type: ScanType | str | None = None,
        source: str | None = None,
        scan_name: str | None = None,
        date_from: datetime | None = None,
        date_to: datetime | None = None,
        page_size: int | None = None,
        max_results: int | None = None,
    ) -> List[ScanRecord]:
        resolved_page_size = self._resolve_page_size(page_size, field_name="page_size")
        return self._scans.query_records(
            scan_type=scan_type,
            source=source,
            scan_name=scan_name,
            date_from=date_from,
            date_to=date_to,
            page_size=resolved_page_size,
            max_results=max_results,
        )

    def query_source(
        self,
        source: str,
        *,
        scan_type: ScanType | str | None = None,
        page_size: int | None = None,
        max_results: int | None = None,
    ) -> Table:
        resolved_page_size = self._resolve_page_size(page_size, field_name="page_size")
        return self._scans.query_source(
            source,
            scan_type=scan_type,
            page_size=resolved_page_size,
            max_results=max_results,
        )

    def query_scan_name(
        self,
        scan_name: str,
        *,
        scan_type: ScanType | str | None = None,
        page_size: int | None = None,
        max_results: int | None = None,
    ) -> Table:
        resolved_page_size = self._resolve_page_size(page_size, field_name="page_size")
        return self._scans.query_scan_name(
            scan_name,
            scan_type=scan_type,
            page_size=resolved_page_size,
            max_results=max_results,
        )

    def query_date_range(
        self,
        *,
        date_from: datetime | None = None,
        date_to: datetime | None = None,
        scan_type: ScanType | str | None = None,
        page_size: int | None = None,
        max_results: int | None = None,
    ) -> Table:
        resolved_page_size = self._resolve_page_size(page_size, field_name="page_size")
        return self._scans.query_date_range(
            date_from=date_from,
            date_to=date_to,
            scan_type=scan_type,
            page_size=resolved_page_size,
            max_results=max_results,
        )

    def download(
        self,
        record: ScanRecord | Mapping[str, Any] | Row,
        *,
        destination_dir: str | Path | None = None,
        overwrite: bool = False,
    ) -> DownloadResult:
        return self._scans.download(
            record,
            destination_dir=destination_dir,
            overwrite=overwrite,
        )

    def download_by_id(
        self,
        *,
        scan_type: ScanType | str,
        scan_id: int,
        destination_dir: str | Path | None = None,
        file_name: str | None = None,
        overwrite: bool = False,
    ) -> DownloadResult:
        return self._scans.download_by_id(
            scan_type=scan_type,
            scan_id=scan_id,
            destination_dir=destination_dir,
            file_name=file_name,
            overwrite=overwrite,
        )

    def download_by_scan_name(
        self,
        scan_name: str,
        *,
        scan_type: ScanType | str | None = None,
        destination_dir: str | Path | None = None,
        overwrite: bool = False,
    ) -> DownloadResult:
        return self._scans.download_by_scan_name(
            scan_name,
            scan_type=scan_type,
            destination_dir=destination_dir,
            overwrite=overwrite,
        )

    def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        return self._request(method, path, **kwargs)

    def get_json(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
        response = self._request("GET", path, params=params)
        return response.json()

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> "Grad300Client":
        return self

    def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
        self.close()

    def _resolve_page_size(self, value: int | None, *, field_name: str) -> int:
        resolved = self._default_page_size if value is None else value
        if resolved <= 0:
            raise ValueError(f"'{field_name}' must be greater than 0")
        return resolved

    def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
        try:
            response = self._client.request(method, path, **kwargs)
        except httpx.HTTPError as exc:
            raise Grad300Error(
                f"Network error while calling GRAD300 API: {exc}"
            ) from exc

        if response.status_code >= 400:
            raise _build_api_error(response)

        return response


class Grad300(Grad300Client):
    """Alias class for a compact astroquery-like import style."""


def _build_api_base_url(*, base_url: str, api_prefix: str) -> str:
    normalized_base = base_url.rstrip("/")
    normalized_prefix = api_prefix if api_prefix.startswith("/") else f"/{api_prefix}"
    normalized_prefix = normalized_prefix.rstrip("/")

    if normalized_base.endswith(normalized_prefix):
        return normalized_base

    return f"{normalized_base}{normalized_prefix}"


def _build_api_error(response: httpx.Response) -> Grad300APIError:
    message = f"Grad300 API error (HTTP {response.status_code})"
    body: Any | None = None

    try:
        body = response.json()
    except ValueError:
        body = response.text

    if isinstance(body, dict):
        detail = body.get("detail")
        if isinstance(detail, str) and detail:
            message = detail
    elif isinstance(body, str) and body:
        message = body

    path = response.request.url.path
    is_auth_error = response.status_code in {401, 403} or (
        path.endswith("/login/access-token") and response.status_code == 400
    )

    error_cls: type[Grad300APIError]
    if is_auth_error:
        error_cls = AuthenticationError
    else:
        error_cls = Grad300APIError

    return error_cls(
        response.status_code,
        message,
        response_body=body,
    )
