"""
HubClient - Python SDK for aphub API
"""

import httpx
from typing import Optional, List, Union, Callable
from pathlib import Path

from aphub_sdk.schemas import (
    AgentManifestV1,
    Agent,
    PushResponse,
    PullResponse,
    SearchResponse,
    HubError,
    ErrorCode,
    API_VERSION,
    Skill,
    SkillManifestV1,
    SkillPushResponse,
    SkillPullResponse,
    SkillSearchResponse,
)
from pydantic import BaseModel

from aphub_sdk.retry import RetryConfig
from aphub_sdk.auth import TokenManager


class HubClientError(Exception):
    """Hub client error."""
    
    def __init__(self, message: str, code: Optional[ErrorCode] = None, status_code: Optional[int] = None):
        super().__init__(message)
        self.code = code
        self.status_code = status_code


class AuthResponse(BaseModel):
    """Authentication response"""
    access_token: str
    refresh_token: str
    token_type: str = "bearer"
    expires_in: Optional[int] = None


class RateResponse(BaseModel):
    """Rate agent response"""
    success: bool
    message: str


class HubClient:
    """Client for interacting with aphub API."""
    
    def __init__(
        self,
        base_url: str = "https://hub.aipartnerup.com",
        api_key: Optional[str] = None,
        timeout: float = 30.0,
        retry_config: Optional[RetryConfig] = None,
        access_token: Optional[str] = None,
        refresh_token: Optional[str] = None,
        progress_callback: Optional[Callable[[int, int], None]] = None,
    ):
        """
        Initialize HubClient.
        
        Args:
            base_url: Base URL of the hub API
            api_key: Optional API key for authentication
            timeout: Request timeout in seconds
            retry_config: Optional retry configuration
            access_token: Optional access token (for JWT auth)
            refresh_token: Optional refresh token (for JWT auth)
            progress_callback: Optional callback for upload/download progress (bytes_sent, total_bytes)
        """
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self._api_version = API_VERSION
        self.retry_config = retry_config or RetryConfig()
        self.progress_callback = progress_callback
        
        # Token manager for JWT auth
        self.token_manager = None
        if access_token or refresh_token:
            self.token_manager = TokenManager(
                base_url=self.base_url,
                access_token=access_token,
                refresh_token=refresh_token,
            )
        
        self._update_headers()
        
        # Use /v1 as base path (API version is in the path, not headers)
        self.client = httpx.Client(
            base_url=f"{self.base_url}/v1",
            headers=self._headers,
            timeout=timeout,
        )
    
    def _update_headers(self):
        """Update headers with current authentication"""
        self._headers = {"User-Agent": "aphub-sdk/0.1.0"}
        
        # Get valid token (refresh if needed)
        token = None
        if self.token_manager:
            token = self.token_manager.get_valid_token()
        
        # Use token or api_key
        auth_value = token or self.api_key
        if auth_value:
            self._headers["Authorization"] = f"Bearer {auth_value}"
        
        # Update client headers if client exists
        if hasattr(self, 'client'):
            self.client.headers.update(self._headers)
    
    def _handle_response(self, response: httpx.Response) -> dict:
        """Handle API response and raise appropriate errors."""
        if response.status_code >= 400:
            try:
                error_data = response.json()
                if "detail" in error_data:
                    # FastAPI error format
                    error_msg = error_data["detail"]
                    if isinstance(error_msg, dict):
                        error_msg = error_msg.get("error", str(error_msg))
                    raise HubClientError(
                        str(error_msg),
                        code=None,
                        status_code=response.status_code,
                    )
                elif "error" in error_data:
                    # HubError format
                    error = HubError(**error_data)
                    raise HubClientError(error.error, error.code, response.status_code)
                else:
                    raise HubClientError(
                        f"API error: {response.status_code}",
                        status_code=response.status_code,
                    )
            except HubClientError:
                raise
            except Exception:
                # Fallback to HTTP status
                try:
                    response.raise_for_status()
                except httpx.HTTPStatusError as e:
                    raise HubClientError(
                        f"HTTP {response.status_code}: {e.response.text[:200]}",
                        status_code=response.status_code,
                    )
        
        try:
            return response.json()
        except ValueError:
            # Not JSON response (e.g., tar.gz)
            return {"content": response.content}
    
    def pull(
        self,
        name: str,
        tag: str = "latest",
        output_path: Optional[Path] = None,
    ) -> PullResponse:
        """
        Pull an Agent from the registry.
        
        Args:
            name: Agent name (e.g., 'my-agent' or 'namespace/my-agent')
            tag: Agent tag (version), defaults to 'latest'
            output_path: Optional output path for Agent files
        
        Returns:
            PullResponse with agent details
        
        Raises:
            HubClientError: If agent not found or access denied
        """
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                # API returns tar.gz directly, not JSON with download URL
                response = self.client.get(
                    f"/agents/pull/{name}",
                    params={"tag": tag},
                    follow_redirects=True,
                )
                
                if response.status_code >= 400:
                    self._handle_response(response)
                
                # Save files if output_path provided
                if output_path:
                    output_path.mkdir(parents=True, exist_ok=True)
                    archive_path = output_path / f"{name}-{tag}.tar.gz"
                    
                    # Stream download with progress callback
                    content_length = response.headers.get("content-length")
                    total_bytes = int(content_length) if content_length else len(response.content)
                    
                    if self.progress_callback and total_bytes > 0:
                        # Stream download
                        with open(archive_path, "wb") as f:
                            downloaded = 0
                            for chunk in response.iter_bytes():
                                f.write(chunk)
                                downloaded += len(chunk)
                                self.progress_callback(downloaded, total_bytes)
                    else:
                        # Simple write
                        with open(archive_path, "wb") as f:
                            f.write(response.content)
                
                # Try to parse response as JSON for manifest info, but API returns tar.gz
                # For now, return a simple success response
                # TODO: API should return JSON with manifest info, then redirect to files
                return PullResponse(
                    success=True,
                    message="Agent pulled successfully",
                )
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    def push(
        self,
        manifest: Union[AgentManifestV1, Path, dict],
        tag: str = "latest",
        files_path: Optional[Path] = None,
    ) -> PushResponse:
        """
        Push an Agent to the registry.
        
        Args:
            manifest: AgentManifestV1 object, path to agent.yaml, or dict
            tag: Agent tag (version), defaults to 'latest'
            files_path: Path to Agent files directory (will be packaged as tar.gz)
        
        Returns:
            PushResponse with result details
        
        Raises:
            HubClientError: If validation fails or access denied
        """
        import tarfile
        import io
        
        # Parse manifest and extract agent name
        manifest_path = None
        manifest_data = None
        
        if isinstance(manifest, Path):
            manifest_path = manifest
            import yaml
            with open(manifest) as f:
                manifest_data = yaml.safe_load(f)
        elif isinstance(manifest, dict):
            manifest_data = manifest
        elif isinstance(manifest, AgentManifestV1):
            manifest_data = manifest.model_dump(by_alias=True, exclude_none=True)
        
        # Extract agent name from manifest
        if isinstance(manifest_data, dict):
            if "metadata" in manifest_data and "name" in manifest_data["metadata"]:
                agent_name = manifest_data["metadata"]["name"]
            elif "name" in manifest_data:
                agent_name = manifest_data["name"]
            else:
                raise HubClientError("Manifest must contain 'name' or 'metadata.name'")
        else:
            raise HubClientError("Invalid manifest format")
        
        # Prepare manifest file content
        if manifest_path:
            with open(manifest_path, "rb") as f:
                manifest_content = f.read()
        else:
            import yaml
            manifest_content = yaml.dump(manifest_data, default_flow_style=False).encode('utf-8')
        
        # Prepare files archive
        files_content = None
        if files_path and files_path.exists():
            buffer = io.BytesIO()
            with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
                tar.add(files_path, arcname=".")
            buffer.seek(0)
            files_content = buffer.read()
        else:
            # Create empty tar.gz if no files provided
            buffer = io.BytesIO()
            with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
                pass
            buffer.seek(0)
            files_content = buffer.read()
        
        # Prepare multipart form data
        files = {
            "manifest": ("agent.yaml", manifest_content, "application/yaml"),
            "files": ("agent.tar.gz", files_content, "application/gzip"),
        }
        
        # Update headers before request (in case token was refreshed)
        self._update_headers()
        
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                # Send push request with progress callback for large files
                total_size = len(manifest_content) + len(files_content)
                if self.progress_callback and total_size > 1024 * 1024:  # > 1MB
                    # For large files, we'd need to use streaming upload
                    # For now, just call callback after upload
                    response = self.client.post(
                        "/agents/push",
                        params={"name": agent_name, "tag": tag},
                        files=files,
                    )
                    self.progress_callback(total_size, total_size)
                else:
                    response = self.client.post(
                        "/agents/push",
                        params={"name": agent_name, "tag": tag},
                        files=files,
                    )
                
                data = self._handle_response(response)
                
                # Parse response
                # API returns PushResponse with agent dict, but schema expects name/version directly
                if isinstance(data, dict):
                    # Handle API response format
                    if "agent" in data and isinstance(data["agent"], dict):
                        # Extract from agent dict
                        agent_data = data["agent"]
                        data["name"] = agent_data.get("name", agent_name)
                        data["version"] = agent_data.get("tag", tag)
                        data["tag"] = agent_data.get("tag", tag)
                    else:
                        # Ensure required fields are present
                        if "name" not in data:
                            data["name"] = agent_name
                        if "version" not in data:
                            if "tag" in data:
                                data["version"] = data["tag"]
                            else:
                                data["version"] = tag
                        if "tag" not in data:
                            data["tag"] = tag
                    
                    return PushResponse(**data)
                else:
                    return PushResponse(
                        success=True,
                        name=agent_name,
                        version=tag,
                        tag=tag,
                        message="Agent pushed successfully",
                    )
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    def search(
        self,
        query: str,
        framework: Optional[str] = None,
        capabilities: Optional[List[str]] = None,
        page: int = 1,
        page_size: int = 20,
    ) -> SearchResponse:
        """
        Search for Agents.
        
        Args:
            query: Search query
            framework: Filter by framework
            capabilities: Filter by capabilities (not yet supported by API)
            page: Page number (1-indexed)
            page_size: Number of results per page
        
        Returns:
            SearchResponse with results
        """
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                # API uses q, limit, offset instead of query, page_size, page
                params = {
                    "q": query,
                    "limit": page_size,
                    "offset": (page - 1) * page_size,
                }
                if framework:
                    params["framework"] = framework
                
                response = self.client.get(
                    "/search",
                    params=params,
                )
                data = self._handle_response(response)
                
                # Convert API response format to SearchResponse
                # API returns SearchResponse with results list
                # Ensure query field is present
                if "query" not in data:
                    data["query"] = query
                
                return SearchResponse(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    def get_agent(self, name: str) -> Agent:
        """
        Get Agent details.
        
        Args:
            name: Agent name (e.g., 'my-agent' or 'namespace/my-agent')
        
        Returns:
            Agent details
        
        Raises:
            HubClientError: If agent not found
        """
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                response = self.client.get(f"/agents/{name}")
                data = self._handle_response(response)
                return Agent(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    def list_tags(self, name: str) -> List[str]:
        """
        List all tags for an Agent.
        
        Args:
            name: Agent name
        
        Returns:
            List of tag strings
        """
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                response = self.client.get(f"/agents/{name}/tags")
                data = self._handle_response(response)
                
                # API returns TagsResponse with tags list
                if isinstance(data, dict):
                    tags = data.get("tags", [])
                    # If tags is a list of TagInfo objects, extract tag strings
                    if tags and isinstance(tags[0], dict):
                        return [tag.get("tag", tag.get("name", "")) for tag in tags]
                    return tags if isinstance(tags, list) else []
                return []
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    def login(self, username: str, password: str) -> AuthResponse:
        """
        Login to the hub and get authentication tokens.
        
        Args:
            username: Username or email
            password: Password
        
        Returns:
            AuthResponse with access_token and refresh_token
        
        Raises:
            HubClientError: If login fails
        """
        # Login should not use retry logic - authentication failures should not be retried
        # Create a temporary client without auth for login
        timeout_value = 30.0
        if hasattr(self.client, 'timeout'):
            if hasattr(self.client.timeout, 'read'):
                timeout_value = self.client.timeout.read
            elif isinstance(self.client.timeout, (int, float)):
                timeout_value = float(self.client.timeout)
        
        try:
            with httpx.Client(
                base_url=f"{self.base_url}/v1",
                timeout=timeout_value,
            ) as temp_client:
                response = temp_client.post(
                    "/auth/login",
                    json={"username": username, "password": password},
                )

                if response.status_code == 200:
                    data = response.json()
                    return AuthResponse(**data)

                # Handle error response
                try:
                    error_data = response.json()
                    if "detail" in error_data:
                        error_msg = error_data["detail"]
                        if isinstance(error_msg, dict):
                            error_msg = error_msg.get("error", str(error_msg))
                        raise HubClientError(str(error_msg), status_code=response.status_code)
                    raise HubClientError(
                        f"Login failed: {response.text[:200]}",
                        status_code=response.status_code,
                    )
                except ValueError:
                    raise HubClientError(
                        f"Login failed: {response.text[:200]}",
                        status_code=response.status_code,
                    )
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                raise HubClientError("Invalid credentials", ErrorCode.INVALID_CREDENTIALS, 401)
            try:
                error_text = e.response.text[:200]
            except Exception:
                error_text = "Unknown error"
            raise HubClientError(
                f"Login failed: {error_text}",
                status_code=e.response.status_code,
            )
        except httpx.RequestError as e:
            raise HubClientError(f"Connection error: {str(e)}")
    
    def get_manifest(self, name: str, tag: str = "latest") -> AgentManifestV1:
        """
        Get Agent manifest for a specific version.
        
        Args:
            name: Agent name (e.g., 'my-agent' or 'namespace/my-agent')
            tag: Agent tag (version), defaults to 'latest'
        
        Returns:
            AgentManifestV1 object
        
        Raises:
            HubClientError: If agent not found
        """
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                response = self.client.get(
                    f"/agents/{name}/manifest",
                    params={"tag": tag}
                )
                data = self._handle_response(response)
                
                # Parse manifest
                return AgentManifestV1(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    def rate_agent(self, name: str, rating: int, comment: Optional[str] = None) -> RateResponse:
        """
        Rate an Agent.
        
        Args:
            name: Agent name
            rating: Rating value (1-5)
            comment: Optional comment
        
        Returns:
            RateResponse with success status
        
        Raises:
            HubClientError: If rating fails or invalid rating value
        """
        # Validate rating range
        if rating < 1 or rating > 5:
            raise HubClientError("Rating must be between 1 and 5", ErrorCode.VALIDATION_ERROR, 400)
        
        last_exception = None
        
        for attempt in range(self.retry_config.max_retries + 1):
            try:
                # Update headers before request (in case token was refreshed)
                self._update_headers()
                
                response = self.client.post(
                    f"/agents/{name}/rate",
                    json={"rating": rating, "comment": comment}
                )
                data = self._handle_response(response)
                
                return RateResponse(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e
                
                # Check if we should retry
                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code
                    
                    # Don't retry on 4xx errors (except 429)
                    if status_code and status_code >= 400 and status_code < 500 and status_code != 429:
                        raise
                    
                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        # Not retryable, raise immediately
                        raise
                    
                    # Calculate backoff delay
                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    # Max retries reached
                    raise
        
        # Should never reach here
        if last_exception:
            raise last_exception
    
    # =========================================================================
    # Skill operations
    # =========================================================================

    def push_skill(
        self,
        manifest: Union[SkillManifestV1, Path, dict],
        tag: str = "latest",
        files_path: Optional[Path] = None,
    ) -> SkillPushResponse:
        """
        Push a Skill to the registry.

        Args:
            manifest: SkillManifestV1 object, path to skill.yaml, or dict
            tag: Skill tag (version), defaults to 'latest'
            files_path: Path to skill files directory (will be packaged as tar.gz)

        Returns:
            SkillPushResponse with result details
        """
        import tarfile
        import io

        # Parse manifest and extract skill name
        manifest_path = None
        manifest_data = None

        if isinstance(manifest, Path):
            manifest_path = manifest
            import yaml
            with open(manifest) as f:
                manifest_data = yaml.safe_load(f)
        elif isinstance(manifest, dict):
            manifest_data = manifest
        elif isinstance(manifest, SkillManifestV1):
            manifest_data = manifest.model_dump(by_alias=True, exclude_none=True)

        # Extract skill name from manifest
        if isinstance(manifest_data, dict):
            if "metadata" in manifest_data and "name" in manifest_data["metadata"]:
                skill_name = manifest_data["metadata"]["name"]
            elif "name" in manifest_data:
                skill_name = manifest_data["name"]
            else:
                raise HubClientError("Manifest must contain 'name' or 'metadata.name'")
        else:
            raise HubClientError("Invalid manifest format")

        # Prepare manifest file content
        if manifest_path:
            with open(manifest_path, "rb") as f:
                manifest_content = f.read()
        else:
            import yaml
            manifest_content = yaml.dump(manifest_data, default_flow_style=False).encode('utf-8')

        # Prepare files archive
        if files_path and files_path.exists():
            buffer = io.BytesIO()
            with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
                tar.add(files_path, arcname=".")
            buffer.seek(0)
            files_content = buffer.read()
        else:
            buffer = io.BytesIO()
            with tarfile.open(fileobj=buffer, mode="w:gz") as tar:
                pass
            buffer.seek(0)
            files_content = buffer.read()

        files = {
            "manifest": ("skill.yaml", manifest_content, "application/yaml"),
            "files": ("skill.tar.gz", files_content, "application/gzip"),
        }

        last_exception = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                self._update_headers()

                response = self.client.post(
                    "/skills/push",
                    params={"name": skill_name, "tag": tag},
                    files=files,
                )

                data = self._handle_response(response)

                if isinstance(data, dict):
                    if "skill" in data and isinstance(data["skill"], dict):
                        skill_data = data["skill"]
                        data["name"] = skill_data.get("name", skill_name)
                        data["version"] = skill_data.get("tag", tag)
                        data["tag"] = skill_data.get("tag", tag)
                    else:
                        if "name" not in data:
                            data["name"] = skill_name
                        if "version" not in data:
                            data["version"] = data.get("tag", tag)
                        if "tag" not in data:
                            data["tag"] = tag

                    return SkillPushResponse(**data)
                else:
                    return SkillPushResponse(
                        success=True,
                        name=skill_name,
                        version=tag,
                        tag=tag,
                        message="Skill pushed successfully",
                    )
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e

                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code

                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        raise

                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    raise

        if last_exception:
            raise last_exception

    def pull_skill(
        self,
        name: str,
        tag: str = "latest",
        output_path: Optional[Path] = None,
    ) -> SkillPullResponse:
        """
        Pull a Skill from the registry.

        Args:
            name: Skill name (e.g., 'code-forge' or 'namespace/code-forge')
            tag: Skill tag (version), defaults to 'latest'
            output_path: Optional output path for skill files

        Returns:
            SkillPullResponse with skill details
        """
        last_exception = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                self._update_headers()

                response = self.client.get(
                    f"/skills/pull/{name}",
                    params={"tag": tag},
                    follow_redirects=True,
                )

                if response.status_code >= 400:
                    self._handle_response(response)

                if output_path:
                    output_path.mkdir(parents=True, exist_ok=True)
                    archive_path = output_path / f"{name}-{tag}.tar.gz"

                    with open(archive_path, "wb") as f:
                        f.write(response.content)

                return SkillPullResponse(
                    success=True,
                    message="Skill pulled successfully",
                )
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e

                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code

                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        raise

                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    raise

        if last_exception:
            raise last_exception

    def search_skills(
        self,
        query: str,
        platforms: Optional[List[str]] = None,
        page: int = 1,
        page_size: int = 20,
    ) -> SkillSearchResponse:
        """
        Search for Skills.

        Args:
            query: Search query
            platforms: Filter by platforms
            page: Page number (1-indexed)
            page_size: Number of results per page

        Returns:
            SkillSearchResponse with results
        """
        last_exception = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                self._update_headers()

                params = {
                    "q": query,
                    "limit": page_size,
                    "offset": (page - 1) * page_size,
                }
                if platforms:
                    params["platforms"] = ",".join(platforms)

                response = self.client.get(
                    "/skills/search",
                    params=params,
                )
                data = self._handle_response(response)

                if "query" not in data:
                    data["query"] = query

                return SkillSearchResponse(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e

                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code

                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        raise

                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    raise

        if last_exception:
            raise last_exception

    def get_skill(self, name: str) -> Skill:
        """
        Get Skill details.

        Args:
            name: Skill name (e.g., 'code-forge' or 'namespace/code-forge')

        Returns:
            Skill details
        """
        last_exception = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                self._update_headers()

                response = self.client.get(f"/skills/{name}")
                data = self._handle_response(response)
                return Skill(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e

                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code

                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        raise

                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    raise

        if last_exception:
            raise last_exception

    def list_skill_tags(self, name: str) -> List[str]:
        """
        List all tags for a Skill.

        Args:
            name: Skill name

        Returns:
            List of tag strings
        """
        last_exception = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                self._update_headers()

                response = self.client.get(f"/skills/{name}/tags")
                data = self._handle_response(response)

                if isinstance(data, dict):
                    tags = data.get("tags", [])
                    if tags and isinstance(tags[0], dict):
                        return [tag.get("tag", tag.get("name", "")) for tag in tags]
                    return tags if isinstance(tags, list) else []
                return []
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e

                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code

                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        raise

                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    raise

        if last_exception:
            raise last_exception

    def get_skill_manifest(self, name: str, tag: str = "latest") -> SkillManifestV1:
        """
        Get Skill manifest for a specific version.

        Args:
            name: Skill name
            tag: Skill tag (version), defaults to 'latest'

        Returns:
            SkillManifestV1 object
        """
        last_exception = None

        for attempt in range(self.retry_config.max_retries + 1):
            try:
                self._update_headers()

                response = self.client.get(
                    f"/skills/{name}/manifest",
                    params={"tag": tag}
                )
                data = self._handle_response(response)

                return SkillManifestV1(**data)
            except (httpx.RequestError, httpx.HTTPStatusError, HubClientError) as e:
                last_exception = e

                if attempt < self.retry_config.max_retries:
                    status_code = None
                    if isinstance(e, httpx.HTTPStatusError):
                        status_code = e.response.status_code
                    elif isinstance(e, HubClientError) and e.status_code:
                        status_code = e.status_code

                    if status_code and status_code not in self.retry_config.retryable_status_codes:
                        raise

                    import time
                    delay = self.retry_config.backoff_factor * (2 ** attempt)
                    time.sleep(delay)
                else:
                    raise

        if last_exception:
            raise last_exception

    def close(self):
        """Close the HTTP client."""
        self.client.close()
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

