"""MyMiniFactory marketplace adapter.

Wraps the MyMiniFactory REST API v2.  Supports search, model details,
file listing, and file downloads (with API key auth).

Environment variables
---------------------
``KILN_MMF_API_KEY``
    MyMiniFactory API key.  Required for all operations.
"""

from __future__ import annotations

import contextlib
import logging
import os
from pathlib import Path
from typing import Any

import requests

from kiln.marketplaces.base import (
    MarketplaceAdapter,
    MarketplaceAuthError,
    MarketplaceError,
    MarketplaceNotFoundError,
    MarketplaceRateLimitError,
    ModelDetail,
    ModelFile,
    ModelSummary,
)

logger = logging.getLogger(__name__)

_BASE_URL = "https://www.myminifactory.com/api/v2"
_REQUEST_TIMEOUT = 30
_DOWNLOAD_TIMEOUT = 120


class MyMiniFactoryAdapter(MarketplaceAdapter):
    """Marketplace adapter backed by the MyMiniFactory REST API v2.

    Args:
        api_key: MyMiniFactory API key.  Falls back to
            ``KILN_MMF_API_KEY`` env var.
        base_url: Override the API base URL (useful for testing).
        session: Optional :class:`requests.Session` for connection pooling.
    """

    def __init__(
        self,
        api_key: str | None = None,
        *,
        base_url: str = _BASE_URL,
        session: requests.Session | None = None,
    ) -> None:
        self._api_key = api_key or os.environ.get("KILN_MMF_API_KEY", "")
        if not self._api_key:
            raise MarketplaceAuthError(
                "MyMiniFactory API key is required.  Set KILN_MMF_API_KEY or pass api_key= to MyMiniFactoryAdapter."
            )
        self._base_url = base_url.rstrip("/")
        self._session = session or requests.Session()

    @property
    def name(self) -> str:
        return "myminifactory"

    @property
    def display_name(self) -> str:
        return "MyMiniFactory"

    @property
    def supports_upload(self) -> bool:
        return True

    # -- low-level request helper ------------------------------------------

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        timeout: int = _REQUEST_TIMEOUT,
    ) -> Any:
        """Make an authenticated API request and return parsed JSON."""
        url = f"{self._base_url}{path}"
        params = dict(params) if params else {}
        params["key"] = self._api_key

        try:
            resp = self._session.request(
                method,
                url,
                params=params,
                timeout=timeout,
            )
        except requests.ConnectionError as exc:
            raise MarketplaceError(
                f"Connection to MyMiniFactory failed: {exc}",
            ) from exc
        except requests.Timeout as exc:
            raise MarketplaceError(
                f"MyMiniFactory request timed out: {exc}",
            ) from exc

        if resp.status_code == 401:
            raise MarketplaceAuthError(
                "Invalid or expired MyMiniFactory API key.",
                status_code=401,
            )
        if resp.status_code == 404:
            raise MarketplaceNotFoundError(
                f"Resource not found: {path}",
                status_code=404,
            )
        if resp.status_code == 429:
            raise MarketplaceRateLimitError(
                "MyMiniFactory API rate limit exceeded.",
                status_code=429,
            )
        if resp.status_code >= 400:
            raise MarketplaceError(
                f"MyMiniFactory API error {resp.status_code}: {resp.text[:200]}",
                status_code=resp.status_code,
            )

        return resp.json()

    # -- MarketplaceAdapter interface --------------------------------------

    def search(
        self,
        query: str,
        *,
        page: int = 1,
        per_page: int = 20,
        sort: str = "relevant",
    ) -> list[ModelSummary]:
        sort_map = {
            "relevant": "popularity",
            "popular": "visits",
            "newest": "date",
        }
        data = self._request(
            "GET",
            "/search",
            params={
                "q": query,
                "page": page,
                "per_page": min(per_page, 100),
                "sort": sort_map.get(sort, sort),
            },
        )
        items = data.get("items", []) if isinstance(data, dict) else []
        return [self._parse_summary(item) for item in items if isinstance(item, dict)]

    def get_details(self, model_id: str) -> ModelDetail:
        data = self._request("GET", f"/objects/{model_id}")
        return self._parse_detail(data)

    def get_files(self, model_id: str) -> list[ModelFile]:
        data = self._request("GET", f"/objects/{model_id}/files")
        items = data.get("items", []) if isinstance(data, dict) else []
        return [self._parse_file(f) for f in items if isinstance(f, dict)]

    def download_file(
        self,
        file_id: str,
        dest_dir: str,
        *,
        file_name: str | None = None,
    ) -> str:
        # Get file metadata for the download URL
        meta = self._request("GET", f"/files/{file_id}")
        download_url = meta.get("download_url", "")
        if not download_url:
            raise MarketplaceError(
                f"No download URL for file {file_id}.  MyMiniFactory may require OAuth2 authentication for downloads.",
            )

        raw_name = file_name or meta.get("filename", f"file_{file_id}")
        # Sanitize: strip directory components to prevent path traversal
        name = os.path.basename(raw_name)
        if not name or name in {".", ".."}:
            name = f"file_{file_id}"
        dest = Path(dest_dir)
        out_path = dest / name

        from kiln.marketplaces.base import resumable_download

        try:
            return resumable_download(
                self._session,
                download_url,
                out_path,
                params={"key": self._api_key},
                timeout=_DOWNLOAD_TIMEOUT,
            )
        except MarketplaceError:
            raise
        except Exception as exc:
            raise MarketplaceError(
                f"Failed to download file {file_id}: {exc}",
            ) from exc

    def upload_model(
        self,
        *,
        file_path: str,
        title: str,
        description: str,
        tags: list[str],
        category: str,
        license_type: str,
    ) -> dict[str, Any]:
        """Create a new object on MyMiniFactory and upload the model file.

        Uses the MyMiniFactory REST API v2:
        1. POST /objects — create the object with metadata
        2. POST /objects/{id}/files — upload the STL/3MF file
        3. PATCH /objects/{id} — publish the object
        """
        if not os.path.isfile(file_path):
            raise MarketplaceError(f"File not found: {file_path}")

        _LICENSE_MAP = {
            "cc-by": "cc-by",
            "cc-by-sa": "cc-by-sa",
            "cc-by-nc": "cc-by-nc",
            "gpl": "gpl",
            "public_domain": "cc0",
        }

        # Step 1: Create the object
        url = f"{self._base_url}/objects"
        try:
            resp = self._session.post(
                url,
                params={"key": self._api_key},
                json={
                    "name": title,
                    "description": description,
                    "tags": tags,
                    "category": category,
                    "license": _LICENSE_MAP.get(license_type, "cc-by"),
                    "visibility": "public",
                },
                timeout=_REQUEST_TIMEOUT,
            )
        except requests.ConnectionError as exc:
            raise MarketplaceError(f"Connection to MyMiniFactory failed: {exc}") from exc
        except requests.Timeout as exc:
            raise MarketplaceError(f"MyMiniFactory request timed out: {exc}") from exc

        if resp.status_code == 401:
            raise MarketplaceAuthError("Invalid or expired MyMiniFactory API key.", status_code=401)
        if resp.status_code >= 400:
            raise MarketplaceError(
                f"Failed to create object ({resp.status_code}): {resp.text[:200]}",
                status_code=resp.status_code,
            )

        data = resp.json()
        object_id = data.get("id")
        if not object_id:
            raise MarketplaceError("MyMiniFactory did not return an object ID.")

        # Step 2: Upload the file
        filename = os.path.basename(file_path)
        try:
            with open(file_path, "rb") as fh:
                resp = self._session.post(
                    f"{self._base_url}/objects/{object_id}/files",
                    params={"key": self._api_key},
                    files={"file": (filename, fh)},
                    timeout=_DOWNLOAD_TIMEOUT,
                )
        except requests.ConnectionError as exc:
            raise MarketplaceError(f"File upload connection failed: {exc}") from exc
        except requests.Timeout as exc:
            raise MarketplaceError(f"File upload timed out: {exc}") from exc

        if resp.status_code >= 400:
            raise MarketplaceError(
                f"File upload failed ({resp.status_code}): {resp.text[:200]}",
                status_code=resp.status_code,
            )

        # Step 3: Publish the object (non-fatal if it fails)
        with contextlib.suppress(requests.ConnectionError, requests.Timeout):
            self._session.patch(
                f"{self._base_url}/objects/{object_id}",
                params={"key": self._api_key},
                json={"status": "published"},
                timeout=_REQUEST_TIMEOUT,
            )

        object_url = data.get("url", f"https://www.myminifactory.com/object/{object_id}")
        logger.info("Published to MyMiniFactory: %s", object_url)
        return {"id": str(object_id), "url": object_url}

    # -- parsing helpers ---------------------------------------------------

    @staticmethod
    def _parse_summary(data: dict[str, Any]) -> ModelSummary:
        designer = data.get("designer", {}) or {}
        licenses = data.get("licenses", [])
        license_str = ""
        if isinstance(licenses, list) and licenses:
            first = licenses[0]
            if isinstance(first, dict):
                license_str = first.get("value", first.get("label", ""))
            elif isinstance(first, str):
                license_str = first

        return ModelSummary(
            id=str(data.get("id", "")),
            name=data.get("name", ""),
            url=data.get("url", ""),
            creator=designer.get("name", designer.get("username", "")),
            source="myminifactory",
            thumbnail=(data.get("images", [{}]) or [{}])[0].get("thumbnail", {}).get("url")
            if data.get("images")
            else None,
            like_count=data.get("likes", 0),
            download_count=data.get("views", 0),  # MMF reports views, not downloads
            license=license_str,
        )

    @staticmethod
    def _parse_detail(data: dict[str, Any]) -> ModelDetail:
        designer = data.get("designer", {}) or {}
        licenses = data.get("licenses", [])
        license_str = ""
        if isinstance(licenses, list) and licenses:
            first = licenses[0]
            if isinstance(first, dict):
                license_str = first.get("value", first.get("label", ""))
            elif isinstance(first, str):
                license_str = first

        tags_raw = data.get("tags", [])
        tags = []
        if isinstance(tags_raw, list):
            for t in tags_raw:
                if isinstance(t, dict):
                    tags.append(t.get("name", t.get("label", "")))
                elif isinstance(t, str):
                    tags.append(t)

        categories = data.get("categories", [])
        category = None
        if isinstance(categories, list) and categories:
            first = categories[0]
            if isinstance(first, dict):
                category = first.get("name", "")
            elif isinstance(first, str):
                category = first

        return ModelDetail(
            id=str(data.get("id", "")),
            name=data.get("name", ""),
            url=data.get("url", ""),
            creator=designer.get("name", designer.get("username", "")),
            source="myminifactory",
            description=data.get("description", ""),
            instructions=data.get("printing_details", ""),
            license=license_str,
            thumbnail=(data.get("images", [{}]) or [{}])[0].get("thumbnail", {}).get("url")
            if data.get("images")
            else None,
            like_count=data.get("likes", 0),
            download_count=data.get("views", 0),
            category=category,
            tags=tags,
            file_count=len(data.get("files", [])),
        )

    @staticmethod
    def _parse_file(data: dict[str, Any]) -> ModelFile:
        filename = data.get("filename", "")
        ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
        return ModelFile(
            id=str(data.get("id", "")),
            name=filename,
            size_bytes=data.get("size", 0),
            download_url=data.get("download_url", ""),
            thumbnail_url=data.get("thumbnail_url", ""),
            file_type=ext,
        )
