"""
SkillDepot Python SDK — Full API client for the SkillDepot AI Skills Marketplace.

Usage:
    from skilldepot import SkillDepot

    client = SkillDepot(api_key="sk_live_...")
    skills = client.list_skills(category="NLP", limit=10)

    # Async usage:
    import asyncio
    from skilldepot import AsyncSkillDepot

    async def main():
        client = AsyncSkillDepot(api_key="sk_live_...")
        skills = await client.list_skills()
        print(skills.data)

    asyncio.run(main())
"""

import httpx
from typing import List, Optional, Dict, Any
from .models import (
    Skill,
    SkillListResponse,
    SkillDetailResponse,
    ReviewResponse,
    PackageResponse,
    APIKeyResponse,
    RunResponse,
    ErrorResponse,
)


class SkillDepotError(Exception):
    """Base exception for SkillDepot SDK errors."""

    def __init__(self, message: str, status_code: int = 0, response: Optional[Dict] = None):
        self.message = message
        self.status_code = status_code
        self.response = response
        super().__init__(self.message)


class AuthenticationError(SkillDepotError):
    """Raised when authentication fails (401)."""
    pass


class AccessDeniedError(SkillDepotError):
    """Raised when access is denied (403)."""
    pass


class NotFoundError(SkillDepotError):
    """Raised when a resource is not found (404)."""
    pass


class RateLimitError(SkillDepotError):
    """Raised when rate limit is exceeded (429)."""
    pass


def _handle_error(response: httpx.Response) -> None:
    """Parse error response and raise appropriate exception."""
    try:
        body = response.json()
        message = body.get("error", response.reason_phrase or "Unknown error")
    except Exception:
        message = response.reason_phrase or f"HTTP {response.status_code}"

    if response.status_code == 401:
        raise AuthenticationError(message, response.status_code)
    elif response.status_code == 403:
        raise AccessDeniedError(message, response.status_code)
    elif response.status_code == 404:
        raise NotFoundError(message, response.status_code)
    elif response.status_code == 429:
        raise RateLimitError(message, response.status_code)
    else:
        raise SkillDepotError(message, response.status_code)


class SkillDepot:
    """
    Synchronous SkillDepot API client.

    Args:
        api_key: Your API key (sk_live_...). Required for authenticated endpoints.
        base_url: Override the API base URL. Defaults to https://api.skilldepot.dev.
        timeout: Request timeout in seconds. Defaults to 30.
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = "https://api.skilldepot.dev",
        timeout: float = 30.0,
    ):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": "skilldepot-python/0.1.0",
        }
        if api_key:
            self._headers["Authorization"] = f"Bearer {api_key}"

    def _request(self, method: str, path: str, params: Optional[Dict] = None, json: Optional[Dict] = None) -> Dict:
        with httpx.Client(timeout=self.timeout) as client:
            response = client.request(
                method,
                f"{self.base_url}{path}",
                params=params,
                json=json,
                headers=self._headers,
            )
            if response.status_code >= 400:
                _handle_error(response)
            return response.json()

    # ─── Skills ───

    def list_skills(
        self,
        category: Optional[str] = None,
        q: Optional[str] = None,
        free: bool = False,
        limit: int = 20,
        offset: int = 0,
    ) -> SkillListResponse:
        """
        List all active skills with optional filters.

        Args:
            category: Filter by category name (e.g., "NLP", "Computer Vision").
            q: Full-text search query.
            free: If True, return only free skills.
            limit: Maximum number of results (1-100). Defaults to 20.
            offset: Pagination offset. Defaults to 0.

        Returns:
            SkillListResponse with data, total count, limit, and offset.
        """
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if category:
            params["category"] = category
        if q:
            params["q"] = q
        if free:
            params["free"] = "true"

        data = self._request("GET", "/api/v1/skills", params=params)
        return SkillListResponse(**data)

    def get_skill(self, slug: str, include_content: bool = False) -> SkillDetailResponse:
        """
        Get a skill by its slug.

        Args:
            slug: The skill's URL slug.
            include_content: If True, include the skill's full .md content.
                Requires authentication for paid skills.

        Returns:
            SkillDetailResponse with the skill data.
        """
        params = {}
        if include_content:
            params["content"] = "true"

        data = self._request("GET", f"/api/v1/skills/{slug}", params=params)
        return SkillDetailResponse(**data)

    def download_skill(self, slug: str) -> str:
        """
        Download a skill's content (convenience wrapper).

        Args:
            slug: The skill's URL slug.

        Returns:
            The skill's markdown content as a string.

        Raises:
            AccessDeniedError: If the skill is paid and not purchased.
            AuthenticationError: If authentication is required but not provided.
        """
        result = self.get_skill(slug, include_content=True)
        return result.data.content or ""

    # ─── Packages ───

    def get_package(self, package_id: str, include_content: bool = False) -> PackageResponse:
        """
        Get a skill package by its ID.

        Args:
            package_id: The package UUID.
            include_content: If True, include all skill content.

        Returns:
            PackageResponse with the package data.
        """
        params = {}
        if include_content:
            params["content"] = "true"

        data = self._request("GET", f"/api/v1/packages/{package_id}", params=params)
        return PackageResponse(**data)

    # ─── Reviews ───

    def create_review(self, skill_id: str, rating: int, comment: Optional[str] = None) -> ReviewResponse:
        """
        Submit a review for a skill. Requires authentication.

        Args:
            skill_id: The skill UUID to review.
            rating: Rating from 1 to 5.
            comment: Optional review comment text.

        Returns:
            ReviewResponse with the created review.

        Raises:
            ValueError: If rating is not 1-5.
            AuthenticationError: If not authenticated.
        """
        if not 1 <= rating <= 5:
            raise ValueError("Rating must be between 1 and 5")

        payload: Dict[str, Any] = {"skill_id": skill_id, "rating": rating}
        if comment:
            payload["comment"] = comment

        data = self._request("POST", "/api/reviews", json=payload)
        return ReviewResponse(**data)

    # ─── API Keys ───

    def create_api_key(self, name: str) -> APIKeyResponse:
        """
        Create a new API key. Requires authentication via JWT.

        Args:
            name: A descriptive name for the key (max 64 chars).

        Returns:
            APIKeyResponse with the new key data. The full_key field
            is only returned once — store it securely.

        Raises:
            AuthenticationError: If not authenticated.
        """
        if not name or len(name.strip()) == 0:
            raise ValueError("Key name is required")

        data = self._request("POST", "/api/api-keys", json={"name": name.strip()})
        return APIKeyResponse(**data)

    def revoke_api_key(self, key_id: str) -> bool:
        """
        Revoke (deactivate) an API key. Requires authentication.

        Args:
            key_id: The API key UUID to revoke.

        Returns:
            True if the key was successfully revoked.
        """
        data = self._request("DELETE", f"/api/api-keys?id={key_id}")
        return data.get("success", False)

    # ─── Run ───

    def run(self, slug: str, input_data: Optional[Dict[str, Any]] = None) -> RunResponse:
        """
        Execute a skill with the given input data.

        Args:
            slug: The skill's URL slug.
            input_data: Key-value input data for the skill.

        Returns:
            RunResponse with the execution result.
        """
        payload = {"input": input_data or {}}
        data = self._request("POST", f"/api/v1/run/{slug}", json=payload)
        return RunResponse(**data)

    # ─── Iteration Helpers ───

    def iter_skills(
        self,
        category: Optional[str] = None,
        q: Optional[str] = None,
        free: bool = False,
        page_size: int = 20,
    ):
        """
        Iterate through all skills with automatic pagination.

        Yields:
            Skill objects one at a time.
        """
        offset = 0
        while True:
            page = self.list_skills(
                category=category, q=q, free=free, limit=page_size, offset=offset
            )
            for skill in page.data:
                yield skill
            offset += page_size
            if offset >= page.total:
                break


class AsyncSkillDepot:
    """
    Async SkillDepot API client — same interface as SkillDepot but with async/await.

    Usage:
        async with AsyncSkillDepot(api_key="sk_live_...") as client:
            skills = await client.list_skills()
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = "https://api.skilldepot.dev",
        timeout: float = 30.0,
    ):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": "skilldepot-python/0.1.0",
        }
        if api_key:
            self._headers["Authorization"] = f"Bearer {api_key}"
        self._client: Optional[httpx.AsyncClient] = None

    async def __aenter__(self):
        self._client = httpx.AsyncClient(timeout=self.timeout)
        return self

    async def __aexit__(self, *args):
        if self._client:
            await self._client.aclose()
            self._client = None

    async def _request(self, method: str, path: str, params: Optional[Dict] = None, json: Optional[Dict] = None) -> Dict:
        client = self._client or httpx.AsyncClient(timeout=self.timeout)
        close_after = self._client is None
        try:
            response = await client.request(
                method,
                f"{self.base_url}{path}",
                params=params,
                json=json,
                headers=self._headers,
            )
            if response.status_code >= 400:
                _handle_error(response)
            return response.json()
        finally:
            if close_after:
                await client.aclose()

    async def list_skills(self, category: Optional[str] = None, q: Optional[str] = None, free: bool = False, limit: int = 20, offset: int = 0) -> SkillListResponse:
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if category:
            params["category"] = category
        if q:
            params["q"] = q
        if free:
            params["free"] = "true"
        data = await self._request("GET", "/api/v1/skills", params=params)
        return SkillListResponse(**data)

    async def get_skill(self, slug: str, include_content: bool = False) -> SkillDetailResponse:
        params = {}
        if include_content:
            params["content"] = "true"
        data = await self._request("GET", f"/api/v1/skills/{slug}", params=params)
        return SkillDetailResponse(**data)

    async def download_skill(self, slug: str) -> str:
        result = await self.get_skill(slug, include_content=True)
        return result.data.content or ""

    async def get_package(self, package_id: str, include_content: bool = False) -> PackageResponse:
        params = {}
        if include_content:
            params["content"] = "true"
        data = await self._request("GET", f"/api/v1/packages/{package_id}", params=params)
        return PackageResponse(**data)

    async def create_review(self, skill_id: str, rating: int, comment: Optional[str] = None) -> ReviewResponse:
        if not 1 <= rating <= 5:
            raise ValueError("Rating must be between 1 and 5")
        payload: Dict[str, Any] = {"skill_id": skill_id, "rating": rating}
        if comment:
            payload["comment"] = comment
        data = await self._request("POST", "/api/reviews", json=payload)
        return ReviewResponse(**data)

    async def create_api_key(self, name: str) -> APIKeyResponse:
        data = await self._request("POST", "/api/api-keys", json={"name": name.strip()})
        return APIKeyResponse(**data)

    async def revoke_api_key(self, key_id: str) -> bool:
        data = await self._request("DELETE", f"/api/api-keys?id={key_id}")
        return data.get("success", False)

    async def run(self, slug: str, input_data: Optional[Dict[str, Any]] = None) -> RunResponse:
        data = await self._request("POST", f"/api/v1/run/{slug}", json={"input": input_data or {}})
        return RunResponse(**data)

    async def iter_skills(self, category: Optional[str] = None, q: Optional[str] = None, free: bool = False, page_size: int = 20):
        offset = 0
        while True:
            page = await self.list_skills(category=category, q=q, free=free, limit=page_size, offset=offset)
            for skill in page.data:
                yield skill
            offset += page_size
            if offset >= page.total:
                break
