from __future__ import annotations

import io
import uuid
from pathlib import Path
from typing import Any

import httpx

from .exceptions import (
    AuthenticationError,
    InferenceError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)
from .models import (
    BatchResult,
    ClassInfo,
    ModelClasses,
    ModelInfo,
    ModelVersion,
    Prediction,
    PredictResult,
)

_API_BASE = "https://api.ssyz.org.tr/api/v1"
_BATCH_TIMEOUT = 120.0


def _auth_headers(key: str) -> dict[str, str]:
    if key.startswith("evren_"):
        return {"X-API-Key": key}
    return {"Authorization": f"Bearer {key}"}


def _raise_for(resp: httpx.Response) -> None:
    code = resp.status_code
    try:
        msg = resp.json().get("detail", resp.text[:300])
    except Exception:
        msg = resp.text[:300]

    if code in (401, 403):
        raise AuthenticationError(msg, code)
    if code == 404:
        raise NotFoundError(msg, code)
    if code == 422:
        raise ValidationError(msg, code)
    if code == 429:
        wait = int(resp.headers.get("Retry-After", "5"))
        raise RateLimitError(msg, wait)
    if code >= 500:
        raise InferenceError(msg, code)
    resp.raise_for_status()


def _read_img(source: str | Path | bytes) -> tuple[bytes, str]:
    if isinstance(source, bytes):
        return source, "image.jpg"
    p = Path(source)
    return p.read_bytes(), p.name


def _is_uuid(val: str) -> bool:
    try:
        uuid.UUID(val)
        return True
    except (ValueError, AttributeError):
        return False


def _unwrap(data: Any) -> Any:
    if isinstance(data, dict) and "data" in data:
        return data["data"]
    return data


def _build_form(version_id: str, confidence: float,
                iou: float, imgsz: int,
                classes: list[str] | None) -> dict[str, str]:
    d: dict[str, str] = {
        "model_version_id": version_id,
        "confidence_threshold": str(confidence),
        "iou_threshold": str(iou),
        "image_size": str(imgsz),
    }
    if classes:
        d["classes"] = ",".join(classes)
    return d


def _to_prediction(raw: dict) -> Prediction:
    return Prediction(
        class_name=raw.get("class_name", ""),
        confidence=raw.get("confidence", 0.0),
        bbox=raw.get("bbox", []),
        color=raw.get("color"),
        keypoints=raw.get("keypoints"),
        mask=raw.get("mask"),
        obb=raw.get("obb"),
        task=raw.get("task"),
        probs=raw.get("probs"),
    )


def _to_result(body: dict) -> PredictResult:
    return PredictResult(
        predictions=[_to_prediction(p) for p in body.get("predictions", [])],
        inference_ms=body.get("inference_ms", 0),
        image_width=body.get("image_width"),
        image_height=body.get("image_height"),
        model_version_id=body.get("model_version_id"),
    )


def _to_model_info(m: dict) -> ModelInfo:
    return ModelInfo(
        id=m["id"], name=m["name"], slug=m.get("slug", ""),
        architecture=m.get("architecture"),
        owner_username=m.get("owner_username"),
    )


def _to_model_ver(v: dict) -> ModelVersion:
    return ModelVersion(
        id=v["id"], version_tag=v.get("version_tag", ""),
        weights_url=v.get("weights_url"),
        framework=v.get("framework", "pytorch"),
        metrics=v.get("metrics", {}),
    )


def _resolve_versions(body: Any) -> list[dict]:
    data = _unwrap(body)
    if isinstance(data, list):
        return data
    return data.get("items", data.get("data", []))


def _pick_version(versions: list[dict], tag: str | None, slug: str) -> str:
    if not versions:
        raise NotFoundError(f"'{slug}' icin versiyon yok", 404)

    if tag is None:
        return versions[0]["id"]

    for v in versions:
        if v.get("version_tag") == tag:
            return v["id"]
    raise NotFoundError(f"'{slug}' icin '{tag}' versiyonu yok", 404)


# ---------------------------------------------------------------
# sync
# ---------------------------------------------------------------


class EvrenClient:
    """EVREN inference API senkron istemcisi.

    >>> with EvrenClient(api_key="evren_xxx") as c:
    ...     r = c.predict("owner/slug", "foto.jpg")
    ...     print(r.predictions)
    """

    def __init__(self, api_key: str, *,
                 base_url: str = _API_BASE,
                 timeout: float = 60.0) -> None:
        self._http = httpx.Client(
            base_url=base_url.rstrip("/"),
            headers=_auth_headers(api_key),
            timeout=timeout,
        )
        self._vcache: dict[str, str] = {}

    def resolve(self, slug: str) -> str:
        """``owner/slug`` veya ``owner/slug:tag`` -> version UUID."""
        if slug in self._vcache:
            return self._vcache[slug]

        slug_part, _, tag = slug.partition(":")
        tag = tag or None

        r = self._http.get(f"/models/{slug_part}")
        if r.status_code != 200:
            _raise_for(r)
        mid = _unwrap(r.json())["id"]

        r2 = self._http.get(f"/models/{mid}/versions")
        if r2.status_code != 200:
            _raise_for(r2)

        vid = _pick_version(_resolve_versions(r2.json()), tag, slug)
        self._vcache[slug] = vid
        return vid

    def _vid(self, model: str) -> str:
        return model if _is_uuid(model) else self.resolve(model)

    def predict(self, model: str, image: str | Path | bytes, *,
                confidence: float = 0.25, iou: float = 0.45,
                image_size: int = 640,
                classes: list[str] | None = None) -> PredictResult:
        raw, name = _read_img(image)
        r = self._http.post(
            "/inference/predict",
            data=_build_form(self._vid(model), confidence, iou, image_size, classes),
            files={"file": (name, io.BytesIO(raw), "image/jpeg")},
        )
        if r.status_code != 200:
            _raise_for(r)
        return _to_result(_unwrap(r.json()))

    def predict_batch(self, model: str, images: list[str | Path | bytes], *,
                      confidence: float = 0.25, iou: float = 0.45,
                      image_size: int = 640,
                      classes: list[str] | None = None) -> BatchResult:
        vid = self._vid(model)
        parts = []
        for i, img in enumerate(images):
            raw, fname = _read_img(img)
            n = fname if fname != "image.jpg" else f"img_{i}.jpg"
            parts.append(("files", (n, io.BytesIO(raw), "image/jpeg")))

        r = self._http.post(
            "/inference/predict/batch",
            data=_build_form(vid, confidence, iou, image_size, classes),
            files=parts, timeout=_BATCH_TIMEOUT,
        )
        if r.status_code != 200:
            _raise_for(r)

        body = _unwrap(r.json())
        items = [_to_result(x) for x in body.get("results", [])]
        return BatchResult(results=items,
                           total_ms=body.get("total_ms", 0),
                           count=body.get("count", len(items)))

    def model_classes(self, model: str) -> ModelClasses:
        vid = self._vid(model)
        r = self._http.get(f"/inference/model-classes/{vid}")
        if r.status_code != 200:
            _raise_for(r)
        b = _unwrap(r.json())
        return ModelClasses(
            model_version_id=b.get("model_version_id", vid),
            classes=[ClassInfo(name=c["name"], color=c["color"])
                     for c in b.get("classes", [])],
            model_name=b.get("model_name"),
            architecture=b.get("architecture"),
            total=b.get("total", 0),
            imgsz=b.get("imgsz", 640),
        )

    def warmup(self, models: list[str]) -> dict:
        ids = [self._vid(m) for m in models]
        r = self._http.post("/inference/warmup", json=ids,
                            timeout=_BATCH_TIMEOUT)
        if r.status_code != 200:
            _raise_for(r)
        return _unwrap(r.json())

    def list_models(self, limit: int = 50) -> list[ModelInfo]:
        r = self._http.get("/models",
                           params={"limit": limit, "include_public": "true"})
        if r.status_code != 200:
            _raise_for(r)
        body = _unwrap(r.json())
        rows = body if isinstance(body, list) else body.get("items", [])
        return [_to_model_info(m) for m in rows]

    def list_versions(self, model_id: str) -> list[ModelVersion]:
        r = self._http.get(f"/models/{model_id}/versions")
        if r.status_code != 200:
            _raise_for(r)
        return [_to_model_ver(v) for v in _resolve_versions(r.json())]

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> EvrenClient:
        return self

    def __exit__(self, *exc: Any) -> None:
        self.close()


# ---------------------------------------------------------------
# async
# ---------------------------------------------------------------


class AsyncEvrenClient:
    """EVREN inference API asenkron istemcisi.

    >>> async with AsyncEvrenClient(api_key="evren_xxx") as c:
    ...     r = await c.predict("owner/slug", "foto.jpg")
    """

    def __init__(self, api_key: str, *,
                 base_url: str = _API_BASE,
                 timeout: float = 60.0) -> None:
        self._http = httpx.AsyncClient(
            base_url=base_url.rstrip("/"),
            headers=_auth_headers(api_key),
            timeout=timeout,
        )
        self._vcache: dict[str, str] = {}

    async def resolve(self, slug: str) -> str:
        if slug in self._vcache:
            return self._vcache[slug]

        slug_part, _, tag = slug.partition(":")
        tag = tag or None

        r = await self._http.get(f"/models/{slug_part}")
        if r.status_code != 200:
            _raise_for(r)
        mid = _unwrap(r.json())["id"]

        r2 = await self._http.get(f"/models/{mid}/versions")
        if r2.status_code != 200:
            _raise_for(r2)

        vid = _pick_version(_resolve_versions(r2.json()), tag, slug)
        self._vcache[slug] = vid
        return vid

    async def _vid(self, model: str) -> str:
        return model if _is_uuid(model) else await self.resolve(model)

    async def predict(self, model: str, image: str | Path | bytes, *,
                      confidence: float = 0.25, iou: float = 0.45,
                      image_size: int = 640,
                      classes: list[str] | None = None) -> PredictResult:
        raw, name = _read_img(image)
        r = await self._http.post(
            "/inference/predict",
            data=_build_form(await self._vid(model), confidence, iou, image_size, classes),
            files={"file": (name, io.BytesIO(raw), "image/jpeg")},
        )
        if r.status_code != 200:
            _raise_for(r)
        return _to_result(_unwrap(r.json()))

    async def predict_batch(self, model: str, images: list[str | Path | bytes], *,
                            confidence: float = 0.25, iou: float = 0.45,
                            image_size: int = 640,
                            classes: list[str] | None = None) -> BatchResult:
        vid = await self._vid(model)
        parts = []
        for i, img in enumerate(images):
            raw, fname = _read_img(img)
            n = fname if fname != "image.jpg" else f"img_{i}.jpg"
            parts.append(("files", (n, io.BytesIO(raw), "image/jpeg")))

        r = await self._http.post(
            "/inference/predict/batch",
            data=_build_form(vid, confidence, iou, image_size, classes),
            files=parts, timeout=_BATCH_TIMEOUT,
        )
        if r.status_code != 200:
            _raise_for(r)
        body = _unwrap(r.json())
        items = [_to_result(x) for x in body.get("results", [])]
        return BatchResult(results=items,
                           total_ms=body.get("total_ms", 0),
                           count=body.get("count", len(items)))

    async def model_classes(self, model: str) -> ModelClasses:
        vid = await self._vid(model)
        r = await self._http.get(f"/inference/model-classes/{vid}")
        if r.status_code != 200:
            _raise_for(r)
        b = _unwrap(r.json())
        return ModelClasses(
            model_version_id=b.get("model_version_id", vid),
            classes=[ClassInfo(name=c["name"], color=c["color"])
                     for c in b.get("classes", [])],
            model_name=b.get("model_name"),
            architecture=b.get("architecture"),
            total=b.get("total", 0),
            imgsz=b.get("imgsz", 640),
        )

    async def warmup(self, models: list[str]) -> dict:
        ids = [await self._vid(m) for m in models]
        r = await self._http.post("/inference/warmup", json=ids,
                                  timeout=_BATCH_TIMEOUT)
        if r.status_code != 200:
            _raise_for(r)
        return _unwrap(r.json())

    async def list_models(self, limit: int = 50) -> list[ModelInfo]:
        r = await self._http.get("/models",
                                 params={"limit": limit, "include_public": "true"})
        if r.status_code != 200:
            _raise_for(r)
        body = _unwrap(r.json())
        rows = body if isinstance(body, list) else body.get("items", [])
        return [_to_model_info(m) for m in rows]

    async def close(self) -> None:
        await self._http.aclose()

    async def __aenter__(self) -> AsyncEvrenClient:
        return self

    async def __aexit__(self, *exc: Any) -> None:
        await self.close()
