"""
Minimal client for the ProSolutionX CDN.

    cdn = CdnClient(api_key=os.environ["CDN_API_KEY"])
    asset = cdn.upload("photo.jpg", slug="blue-widget-hero")
    print(asset["url"])

Python 3.9+ — requires the requests package.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any, BinaryIO
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse

import requests

BASE_URL = "https://cdn.prosolutionx.com"


class CdnError(RuntimeError):
    def __init__(self, message: str, status: int = 0, body: Any = None) -> None:
        super().__init__(message)
        self.status = status
        self.body = body


class CdnClient:
    def __init__(self, api_key: str, base_url: str = BASE_URL, timeout: int = 120) -> None:
        if not api_key:
            raise ValueError("api_key is required")
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key}"

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        response = self.session.request(
            method, self.base_url + path, timeout=self.timeout, **kwargs
        )
        try:
            body = response.json() if response.content else None
        except ValueError:
            body = response.text
        if response.status_code >= 400:
            message = body.get("error") if isinstance(body, dict) else None
            raise CdnError(message or f"HTTP {response.status_code}", response.status_code, body)
        return body

    def me(self) -> dict:
        """Who am I — a cheap way to check a key is live."""
        return self._request("GET", "/api/v1/me")

    def upload(
        self,
        file: str | Path | BinaryIO,
        *,
        slug: str | None = None,
        alt: str | None = None,
        title: str | None = None,
        filename: str | None = None,
    ) -> dict:
        """Upload a file. The slug becomes the readable part of the URL;
        omit it and the filename is used instead."""
        close_after = False
        if isinstance(file, (str, Path)):
            filename = filename or os.path.basename(str(file))
            handle: BinaryIO = open(file, "rb")
            close_after = True
        else:
            handle = file
            filename = filename or "upload"

        data = {k: v for k, v in (("slug", slug), ("alt", alt), ("title", title)) if v}
        try:
            return self._request(
                "POST", "/api/v1/assets", files={"file": (filename, handle)}, data=data
            )
        finally:
            if close_after:
                handle.close()

    def list(self, limit: int = 50, cursor: int | None = None, q: str | None = None) -> dict:
        params: dict[str, Any] = {"limit": limit}
        if cursor is not None:
            params["cursor"] = cursor
        if q is not None:
            params["q"] = q
        return self._request("GET", "/api/v1/assets?" + urlencode(params))

    def get(self, id_or_slug: str | int) -> dict:
        return self._request("GET", f"/api/v1/assets/{id_or_slug}")

    def update(self, id_or_slug: str | int, **changes: Any) -> dict:
        """Rename, or change alt / title. Renaming changes the readable URL."""
        return self._request("PATCH", f"/api/v1/assets/{id_or_slug}", json=changes)

    def replace(self, id_or_slug: str | int, file: str | Path) -> dict:
        """Replace the bytes behind an existing name."""
        with open(file, "rb") as handle:
            return self._request(
                "PUT",
                f"/api/v1/assets/{id_or_slug}",
                files={"file": (os.path.basename(str(file)), handle)},
            )

    def delete(self, id_or_slug: str | int) -> dict:
        return self._request("DELETE", f"/api/v1/assets/{id_or_slug}")

    @staticmethod
    def transform(url: str, *, w: int | None = None, f: str | None = None, q: int | None = None) -> str:
        """Build a resized / converted URL from one the API returned."""
        parts = urlparse(url)
        query = dict(parse_qsl(parts.query))
        for key, value in (("w", w), ("f", f), ("q", q)):
            if value is not None:
                query[key] = str(value)
        return urlunparse(parts._replace(query=urlencode(query)))
