"""
FormFromLight · single-file Python SDK
---------------------------------------
Python 3.8+ · requires only the `requests` package (`pip install requests`).

Usage:

    from formfromlight import FormFromLight

    ffl = FormFromLight(api_key="...")
    me = ffl.whoami()

    job = ffl.generate_from_image(
        image_url="https://example.com/sword.png",
        ai_model="tripo-h31",
        quality="standard",
        style="realistic",
    )
    model = ffl.wait_for_completion(job["id"], timeout_s=120)
    print(model["urls"]["glb"])

Errors:
    Raises `FFLApiError` (subclass of RuntimeError) on 4xx/5xx with .status,
    .body, and .response (the raw `requests.Response`).
"""
from __future__ import annotations

import time
from typing import Any, Dict, Iterable, Mapping, Optional

try:
    import requests
except ImportError as e:  # pragma: no cover
    raise RuntimeError(
        "formfromlight requires the `requests` package. Run: pip install requests"
    ) from e

__all__ = ["FormFromLight", "FFLApiError"]
__version__ = "1.0.0"

# These literals mirror the TypeScript SDK so type-aware editors get hints.
AI_MODELS: Iterable[str] = ("trellis", "tripo-h31", "rodin")
QUALITIES: Iterable[str] = ("draft", "standard", "high", "ultra")
STYLES: Iterable[str] = ("realistic", "stylized", "toon", "anime", "pbrmetal")
ANIMATIONS: Iterable[str] = (
    "idle", "walk", "run", "jump", "dance", "wave", "punch", "kick",
)


class FFLApiError(RuntimeError):
    """Raised when the API returns a non-2xx response."""

    def __init__(self, status: int, body: Any, response: "requests.Response"):
        self.status = status
        self.body = body
        self.response = response
        msg = (
            body.get("error")
            if isinstance(body, dict) and "error" in body
            else f"HTTP {status}"
        )
        super().__init__(msg)


class FormFromLight:
    """Thin wrapper around the FormFromLight REST API."""

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://formfromlight.com/api/v1",
        session: Optional["requests.Session"] = None,
        timeout: float = 30.0,
    ):
        if not api_key:
            raise ValueError("FormFromLight: api_key is required")
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._session = session or requests.Session()

    # ----- account -----

    def whoami(self) -> Dict[str, Any]:
        return self._req("GET", "/whoami")

    # ----- models -----

    def list_models(self, limit: int = 25) -> Dict[str, Any]:
        return self._req("GET", f"/models?limit={int(limit)}")

    def get_model(self, model_id: int) -> Dict[str, Any]:
        return self._req("GET", f"/models/{int(model_id)}")

    # ----- generation -----

    def generate_from_image(
        self,
        image_url: str,
        ai_model: str,
        quality: str = "standard",
        style: str = "realistic",
        title: Optional[str] = None,
    ) -> Dict[str, Any]:
        body = {
            "image_url": image_url,
            "ai_model": ai_model,
            "quality": quality,
            "style": style,
        }
        if title is not None:
            body["title"] = title
        return self._req("POST", "/generate/image", body)

    def generate_from_text(
        self,
        prompt: str,
        quality: str = "standard",
        style: str = "realistic",
        title: Optional[str] = None,
    ) -> Dict[str, Any]:
        body = {"prompt": prompt, "quality": quality, "style": style}
        if title is not None:
            body["title"] = title
        return self._req("POST", "/generate/text", body)

    def retexture(
        self,
        model_id: int,
        style: str,
        prompt: Optional[str] = None,
    ) -> Dict[str, Any]:
        body: Dict[str, Any] = {"style": style}
        if prompt is not None:
            body["prompt"] = prompt
        return self._req("POST", f"/models/{int(model_id)}/retexture", body)

    def rig(self, model_id: int, animation: Optional[str] = None) -> Dict[str, Any]:
        body: Dict[str, Any] = {}
        if animation is not None:
            body["animation"] = animation
        return self._req("POST", f"/models/{int(model_id)}/rig", body)

    # ----- polling -----

    def wait_for_completion(
        self,
        model_id: int,
        timeout_s: float = 5 * 60,
        interval_s: float = 5.0,
    ) -> Dict[str, Any]:
        """Poll `get_model` until status is `completed` or `failed`."""
        deadline = time.monotonic() + timeout_s
        while time.monotonic() < deadline:
            m = self.get_model(model_id)
            if m.get("status") in ("completed", "failed"):
                return m
            time.sleep(interval_s)
        raise TimeoutError(
            f"FormFromLight: model {model_id} did not settle within {timeout_s}s"
        )

    # ----- internals -----

    def _req(self, method: str, path: str, body: Optional[Mapping[str, Any]] = None) -> Any:
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Accept": "application/json",
        }
        kwargs: Dict[str, Any] = {"headers": headers, "timeout": self.timeout}
        if body is not None:
            kwargs["json"] = body

        r = self._session.request(method, self.base_url + path, **kwargs)
        if not r.ok:
            parsed: Any = None
            try:
                parsed = r.json()
            except Exception:
                pass
            raise FFLApiError(r.status_code, parsed, r)
        return r.json()
