from __future__ import annotations

import hashlib
import json
import time
from typing import Any, Dict, Optional
from urllib.parse import urlencode

import httpx

from ..cache import Cache


class ProviderError(RuntimeError):
    pass


class BaseProvider:
    provider_name = "base"
    min_delay_seconds = 0.0
    timeout_seconds = 10.0
    max_results = 5
    max_retries = 2
    backoff_seconds = 1.0

    def __init__(
        self,
        cache: Cache,
        offline: bool = False,
        user_agent: Optional[str] = None,
    ) -> None:
        self.cache = cache
        self.offline = offline
        self.user_agent = user_agent
        self._last_request = 0.0

    def lookup_by_id(self, identifier: str, id_type: str) -> list[dict]:
        raise NotImplementedError

    def search(self, query: str) -> list[dict]:
        raise NotImplementedError

    def _rate_limit(self) -> None:
        delay = self.min_delay_seconds
        if delay <= 0:
            return
        elapsed = time.monotonic() - self._last_request
        if elapsed < delay:
            time.sleep(delay - elapsed)

    def _make_request_key(self, url: str, params: Optional[Dict[str, Any]]) -> str:
        query = urlencode(sorted((params or {}).items()))
        raw = f"{url}?{query}"
        return hashlib.sha1(raw.encode("utf-8")).hexdigest()

    def _request_json(self, url: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
        request_key = self._make_request_key(url, params)
        cached = self.cache.get(self.provider_name, request_key)
        if cached:
            return {"_cache": "HIT", "status_code": cached.status_code, "data": cached.response_json}
        if self.offline:
            raise ProviderError("Offline mode enabled and no cache entry available.")

        request_headers = headers.copy() if headers else {}
        if self.user_agent:
            request_headers.setdefault("User-Agent", self.user_agent)

        last_error: Optional[str] = None
        for attempt in range(self.max_retries + 1):
            self._rate_limit()
            try:
                with httpx.Client(timeout=self.timeout_seconds) as client:
                    response = client.get(url, params=params, headers=request_headers)
            except httpx.HTTPError as exc:
                last_error = str(exc)
                if attempt < self.max_retries:
                    time.sleep(self.backoff_seconds * (2 ** attempt))
                    continue
                raise ProviderError(last_error) from exc

            self._last_request = time.monotonic()
            status_code = response.status_code

            if status_code == 429 and attempt < self.max_retries:
                retry_after = response.headers.get("Retry-After")
                if retry_after and retry_after.isdigit():
                    time.sleep(float(retry_after))
                else:
                    time.sleep(self.backoff_seconds * (2 ** attempt))
                continue
            if 500 <= status_code < 600 and attempt < self.max_retries:
                time.sleep(self.backoff_seconds * (2 ** attempt))
                continue

            try:
                data = response.json()
            except json.JSONDecodeError:
                data = {"raw": response.text}

            self.cache.set(self.provider_name, request_key, data, status_code)
            return {"_cache": "MISS", "status_code": status_code, "data": data}

        raise ProviderError(last_error or "Request failed")
