"""Synchronous SPARQL client."""

import json
import time
import warnings
from io import BytesIO
from typing import Literal
from urllib.parse import urlencode

import pycurl

from sparqlite.exceptions import EndpointError, QueryError

HTTPMethod = Literal["GET", "POST"]


class SPARQLClient:

    def __init__(
        self,
        endpoint: str,
        *,
        max_retries: int = 5,
        backoff_factor: float = 0.5,
        timeout: float | None = None,
    ):
        self.endpoint = endpoint
        self.max_retries = max_retries
        self.backoff_factor = backoff_factor
        self.timeout = timeout
        self._curl: pycurl.Curl | None = pycurl.Curl()

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

    def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
        self.close()

    def __del__(self) -> None:
        if self._curl is not None:
            warnings.warn(
                "SPARQLClient was not closed. Use 'with SPARQLClient(...) as client:' "
                "or call 'client.close()' explicitly.",
                ResourceWarning,
                stacklevel=2,
            )
        self.close()

    def close(self) -> None:
        if self._curl is not None:
            self._curl.close()
            self._curl = None

    def _request(
        self,
        query: str,
        accept: str,
        *,
        method: HTTPMethod = "GET",
        is_update: bool = False,
    ) -> bytes:
        if self._curl is None:
            raise EndpointError("Client is closed")

        curl = self._curl
        last_error: EndpointError | None = None

        for attempt in range(self.max_retries + 1):
            if attempt > 0:
                wait_time = self.backoff_factor * (2**attempt)
                time.sleep(wait_time)

            buffer = BytesIO()
            curl.reset()

            if self.timeout is not None:
                curl.setopt(pycurl.TIMEOUT_MS, int(self.timeout * 1000))

            try:
                param_key = "update" if is_update else "query"

                if method == "GET":
                    url = f"{self.endpoint}?{urlencode({param_key: query})}"
                    curl.setopt(pycurl.URL, url)
                    curl.setopt(pycurl.HTTPGET, 1)
                else:
                    curl.setopt(pycurl.URL, self.endpoint)
                    curl.setopt(pycurl.POSTFIELDS, urlencode({param_key: query}))

                curl.setopt(pycurl.WRITEDATA, buffer)
                curl.setopt(
                    pycurl.HTTPHEADER,
                    [
                        f"Accept: {accept}",
                        "User-Agent: sparqlite/0.1.0",
                    ],
                )

                curl.perform()

                status_code = curl.getinfo(pycurl.RESPONSE_CODE)

                if status_code == 400:
                    raise QueryError(f"Query syntax error: {buffer.getvalue().decode()}")

                if status_code >= 500:
                    last_error = EndpointError(
                        f"Server error: {status_code}",
                        status_code=status_code,
                    )
                    continue

                if status_code >= 400:
                    raise EndpointError(
                        f"HTTP error: {status_code} - {buffer.getvalue().decode()}",
                        status_code=status_code,
                    )

                return buffer.getvalue()

            except pycurl.error as e:
                error_code, error_msg = e.args
                if error_code in (pycurl.E_COULDNT_CONNECT, pycurl.E_COULDNT_RESOLVE_HOST):
                    last_error = EndpointError(f"Connection error: {error_msg}")
                elif error_code == pycurl.E_OPERATION_TIMEDOUT:
                    last_error = EndpointError(f"Timeout error: {error_msg}")
                else:
                    last_error = EndpointError(f"Request error: {error_msg}")
                continue

        raise last_error  # type: ignore[misc]

    def query(self, query: str, *, method: HTTPMethod = "GET") -> dict:
        content = self._request(
            query, "application/sparql-results+json", method=method
        )
        return json.loads(content)

    def select(self, query: str, *, method: HTTPMethod = "GET") -> dict:
        return self.query(query, method=method)

    def ask(self, query: str, *, method: HTTPMethod = "GET") -> bool:
        content = self._request(
            query, "application/sparql-results+json", method=method
        )
        return json.loads(content)["boolean"]

    def construct(self, query: str, *, method: HTTPMethod = "GET") -> bytes:
        return self._request(query, "application/n-triples", method=method)

    def describe(self, query: str, *, method: HTTPMethod = "GET") -> bytes:
        return self._request(query, "application/n-triples", method=method)

    def update(self, query: str) -> None:
        self._request(
            query, "application/sparql-results+json", method="POST", is_update=True
        )
