"""Tiny dependency-free client for https://cdn.whohouse.io.

Set WHOHOUSE_CDN_TOKEN (or pass token=) before using the control API.
Published response URLs themselves are public.
"""

from __future__ import annotations

import base64
import json
import mimetypes
import os
from pathlib import Path
from typing import BinaryIO
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, urlopen


class CDNError(RuntimeError):
    """A control-plane request failed."""


class CDN:
    def __init__(self, token: str | None = None, base_url: str = "https://cdn.whohouse.io"):
        self.token = token or os.environ.get("WHOHOUSE_CDN_TOKEN") or os.environ.get("CDN_TOKEN")
        if not self.token:
            raise ValueError("Pass token= or set WHOHOUSE_CDN_TOKEN")
        self.base_url = base_url.rstrip("/")

    def serve_file(
        self,
        file: str | os.PathLike[str] | bytes | BinaryIO,
        *,
        host: str | None = None,
        path: str | None = None,
        agent: str | None = None,
        status: int = 200,
        reason: str | None = None,
        content_type: str | None = None,
        response_headers: dict[str, str | list[str]] | list[list[str]] | None = None,
        replace: bool = True,
    ) -> dict:
        """Serve file bytes with useful HTTP defaults.

        A collision-resistant host is generated when host is omitted. A filesystem
        input defaults to /<filename>; byte and stream inputs default to /file.
        """
        filename = None
        if isinstance(file, (str, os.PathLike)):
            source = Path(file)
            filename = source.name
            data = source.read_bytes()
        elif isinstance(file, bytes):
            data = file
        else:
            data = file.read()
        path = path or "/" + (filename or "file")
        content_type = content_type or (mimetypes.guess_type(filename or "")[0] if filename else None) or "application/octet-stream"
        query = {
            "path": path,
            "status": str(status),
            "content_type": content_type,
            "replace": str(replace).lower(),
        }
        if host:
            query["host"] = host
        if agent:
            query["agent"] = agent
        if reason is not None:
            query["reason"] = reason
        headers = {"Content-Type": content_type}
        if response_headers is not None:
            encoded = base64.urlsafe_b64encode(json.dumps(response_headers).encode()).rstrip(b"=").decode()
            headers["X-Whohouse-Response-Headers"] = encoded
        return self._request("PUT", "/v1/file?" + urlencode(query), data, headers)

    def deploy(
        self,
        body: str | bytes = "",
        *,
        host: str | None = None,
        path: str = "/",
        method: str = "*",
        agent: str | None = None,
        status: int = 200,
        reason: str | None = None,
        first_line: str | None = None,
        headers: dict[str, str | list[str]] | list[list[str]] | None = None,
        auto_headers: bool = True,
        content_type: str | None = None,
        replace: bool = True,
    ) -> dict:
        """Publish a structured response, including custom 100..999 status codes."""
        payload: dict = {
            "host": host or "",
            "path": path,
            "method": method,
            "agent": agent or "",
            "status": status,
            "replace": replace,
            "auto_headers": auto_headers,
        }
        if isinstance(body, bytes):
            payload["body_base64"] = base64.b64encode(body).decode()
        else:
            payload["body"] = body
        if reason is not None:
            payload["reason"] = reason
        if first_line is not None:
            payload["first_line"] = first_line
        if headers is not None:
            payload["headers"] = headers
        if content_type is not None:
            payload["content_type"] = content_type
        return self._request("POST", "/v1/deploy", json.dumps(payload).encode(), {"Content-Type": "application/json"})

    def raw(
        self,
        wire: bytes | str,
        *,
        host: str | None = None,
        path: str = "/",
        method: str = "*",
        agent: str | None = None,
        replace: bool = True,
    ) -> dict:
        """Publish exact response bytes; no HTTP validity is required."""
        data = wire.encode() if isinstance(wire, str) else wire
        query = {"path": path, "method": method, "replace": str(replace).lower()}
        if host:
            query["host"] = host
        if agent:
            query["agent"] = agent
        return self._request("PUT", "/v1/raw?" + urlencode(query), data, {"Content-Type": "application/octet-stream"})

    def new_host(self, requested: str | None = None) -> dict:
        query = "?" + urlencode({"host": requested}) if requested else ""
        return self._request("POST", "/v1/hosts" + query, b"")

    def list_all(self, collection: str = "revisions") -> list[dict]:
        if collection not in {"revisions", "routes", "agents"}:
            raise ValueError("collection must be revisions, routes, or agents")
        output: list[dict] = []
        cursor = ""
        while True:
            query = {"limit": "500"}
            if cursor:
                query["cursor"] = cursor
            page = self._request("GET", f"/v1/{collection}?" + urlencode(query))
            output.extend(page["items"])
            cursor = page.get("next_cursor", "")
            if not cursor:
                return output

    def _request(self, method: str, path: str, data: bytes | None = None, headers: dict[str, str] | None = None) -> dict:
        request_headers = dict(headers or {})
        request_headers["Authorization"] = "Bearer " + self.token
        request = Request(self.base_url + path, data=data, headers=request_headers, method=method)
        try:
            with urlopen(request, timeout=900) as response:
                return json.load(response)
        except HTTPError as error:
            try:
                detail = json.loads(error.read())
            except (json.JSONDecodeError, UnicodeDecodeError):
                detail = {"message": error.reason}
            raise CDNError(f"{error.code}: {detail.get('message', error.reason)}") from error
