"""Async client for the Danube SDK."""

import logging
from typing import Any, Dict, List, Optional

from danube._http import HTTPClient
from danube._utils import clean_id, is_valid_uuid
from danube.config import DanubeConfig
from danube.exceptions import (
    AuthenticationError,
    ConfigurationRequiredError,
    ExecutionError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)
from danube.models import (
    AgentFundResult,
    AgentInfo,
    AgentRegistration,
    AgentSite,
    AgentSiteListItem,
    APIKeyResponse,
    APIKeyWithSecret,
    Contact,
    CredentialStoreResult,
    DeviceCode,
    DeviceToken,
    Identity,
    RatingAggregate,
    Service,
    ServiceToolsResult,
    Skill,
    SkillContent,
    SpendingLimits,
    Tool,
    ToolRating,
    ToolResult,
    WalletBalance,
    WalletTransaction,
    WebhookCreateResponse,
    WebhookDeliveryResponse,
    WebhookResponse,
    Workflow,
    WorkflowExecution,
    WorkflowCreateRequest,
)

logger = logging.getLogger("danube")


class AgentsResource:
    """Operations on autonomous agents."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def register(
        self,
        name: str,
        operator_email: str,
    ) -> AgentRegistration:
        """Register a new autonomous agent.

        Creates a user profile, API key, and USDC wallet in one call.
        The API key is returned only once.

        No authentication required (public endpoint).

        Args:
            name: Display name for the agent.
            operator_email: Contact email for the operator.

        Returns:
            AgentRegistration with agent_id, api_key (one-time), wallet_id, etc.
        """
        data = await self._client._http.public_post(
            "/v1/agents",
            json={"name": name, "operator_email": operator_email},
        )
        return AgentRegistration.from_api_response(data)

    async def get_info(self) -> AgentInfo:
        """Get the current agent's profile and wallet balance.

        Only works for agent accounts (not human user accounts).

        Returns:
            AgentInfo with wallet balance, deposit address, etc.
        """
        data = await self._client._http.get("/v1/agents/me")
        return AgentInfo.from_api_response(data)

    async def fund_wallet(
        self,
        method: str,
        amount_cents: Optional[int] = None,
    ) -> AgentFundResult:
        """Fund the agent's wallet.

        Args:
            method: "card_checkout" or "crypto".
            amount_cents: Amount in cents (required for card_checkout, min 100).

        Returns:
            AgentFundResult with checkout_url or deposit_address.
        """
        payload: Dict[str, Any] = {"method": method}
        if amount_cents is not None:
            payload["amount_cents"] = amount_cents
        data = await self._client._http.post("/v1/agents/wallets/fund", json=payload)
        return AgentFundResult.from_api_response(data)


class ServicesResource:
    """Operations on services."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def list(
        self,
        query: str = "",
        limit: int = 10,
    ) -> List[Service]:
        """List or search available services.

        Args:
            query: Optional search query to filter by name/description.
            limit: Maximum number of results (default 10).

        Returns:
            List of matching Service objects.
        """
        params = {"num_results": limit}
        if query:
            params["query"] = query

        data = await self._client._http.get("/v1/services", params=params)

        # Handle both list and dict responses
        services_data = data if isinstance(data, list) else data.get("services", [])

        return [Service.from_api_response(s) for s in services_data]

    async def get(self, service_id: str) -> Service:
        """Get a service by ID.

        Args:
            service_id: The service UUID.

        Returns:
            Service details.

        Raises:
            NotFoundError: If service doesn't exist.
        """
        data = await self._client._http.get(f"/v1/services/{service_id}")
        return Service.from_api_response(data)

    async def get_tools(
        self,
        service_id: str,
        limit: int = 50,
    ) -> ServiceToolsResult:
        """Get all tools for a service.

        Args:
            service_id: The service UUID.
            limit: Maximum tools to return (default 50).

        Returns:
            ServiceToolsResult with tools and configuration status.
        """
        data = await self._client._http.get(
            f"/v1/services/{service_id}/tools",
            params={"limit": limit},
        )

        # Handle extended response format
        if isinstance(data, dict):
            if data.get("needs_configuration"):
                config_info = data.get("configuration_info", {})
                return ServiceToolsResult(
                    tools=[],
                    needs_configuration=True,
                    skipped=False,
                    configuration_required={
                        "service_id": service_id,
                        "service_name": config_info.get("service_name", ""),
                        "message": config_info.get("message", ""),
                        "configuration_url": config_info.get(
                            "configuration_url",
                            f"https://app.danube.ai/dashboard/services/{service_id}",
                        ),
                        "credential_schema": config_info.get("credential_schema"),
                    },
                )
            elif data.get("skipped"):
                return ServiceToolsResult(
                    tools=[],
                    needs_configuration=False,
                    skipped=True,
                    configuration_required=None,
                )
            elif "tools" in data:
                tools_data = data.get("tools", [])
            else:
                tools_data = data if isinstance(data, list) else []
        else:
            tools_data = data if isinstance(data, list) else []

        tools = [Tool.from_api_response(t) for t in tools_data[:limit]]
        return ServiceToolsResult(
            tools=tools,
            needs_configuration=False,
            skipped=False,
            configuration_required=None,
        )


class ToolsResource:
    """Operations on tools."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def search(
        self,
        query: str,
        service_id: Optional[str] = None,
        limit: int = 10,
    ) -> List[Tool]:
        """Search for tools by name or description.

        Uses semantic search to find the most relevant tools.

        Args:
            query: Search query (semantic search).
            service_id: Optional filter by service.
            limit: Maximum results (default 10).

        Returns:
            List of matching Tool objects.
        """
        params: Dict[str, Any] = {"query": query, "num_results": limit}
        if service_id:
            params["service_id"] = service_id

        data = await self._client._http.get("/v1/tools/search", params=params)

        # Handle both list and dict responses
        tools_data = data if isinstance(data, list) else data.get("tools", [])

        return [Tool.from_api_response(t) for t in tools_data[:limit]]

    async def get(self, tool_id: str) -> Tool:
        """Get a tool by ID.

        Args:
            tool_id: The tool UUID.

        Returns:
            Tool details.

        Raises:
            NotFoundError: If tool doesn't exist.
        """
        data = await self._client._http.get(f"/v1/tools/{tool_id}")
        return Tool.from_api_response(data)

    async def execute(
        self,
        tool_id: Optional[str] = None,
        tool_name: Optional[str] = None,
        parameters: Optional[Dict[str, Any]] = None,
    ) -> ToolResult:
        """Execute a tool by ID or name.

        Args:
            tool_id: Tool UUID (preferred, faster).
            tool_name: Tool name (will search for match).
            parameters: Parameters to pass to the tool.

        Returns:
            ToolResult with execution result or error.

        Raises:
            ValidationError: If neither tool_id nor tool_name provided.
            NotFoundError: If tool cannot be found.
            ExecutionError: If tool execution fails.
        """
        if not tool_id and not tool_name:
            raise ValidationError("Either tool_id or tool_name must be provided")

        resolved_tool_id = None
        resolved_tool_name = tool_name

        # If tool_id is provided and is a valid UUID, use it directly
        if tool_id:
            cleaned_id = clean_id(tool_id)
            if cleaned_id and is_valid_uuid(cleaned_id):
                resolved_tool_id = cleaned_id
                logger.debug(f"Executing tool by ID: {resolved_tool_id}")
            else:
                # tool_id is not a valid UUID, treat it as a tool name
                logger.debug(f"'{cleaned_id}' is not a valid UUID, treating as name")
                tool_name = cleaned_id
                resolved_tool_name = cleaned_id

        # If we don't have a resolved ID, search by name
        if not resolved_tool_id:
            logger.debug(f"Searching for tool by name: '{tool_name}'")
            tools = await self.search(query=tool_name or "", limit=5)

            if not tools:
                raise NotFoundError("Tool", tool_name or "")

            # Find exact match or use first result
            tool = None
            for t in tools:
                if t.name == tool_name:
                    tool = t
                    break

            if not tool:
                tool = tools[0]
                logger.debug(
                    f"No exact match for '{tool_name}', using: '{tool.name}'"
                )

            resolved_tool_id = tool.id
            resolved_tool_name = tool.name

        if not resolved_tool_id:
            raise NotFoundError("Tool", tool_id or tool_name or "")

        # Execute the tool
        logger.debug(f"Calling tool: {resolved_tool_id} ({resolved_tool_name})")

        try:
            data = await self._client._http.post(
                f"/v1/tools/call/{resolved_tool_id}",
                json=parameters or {},
            )

            # Handle response
            if data.get("status") == "success":
                result_value = data.get("result", "")
                return ToolResult(
                    success=True,
                    result=result_value,
                    tool_id=resolved_tool_id,
                    tool_name=resolved_tool_name,
                )
            else:
                error_msg = data.get("error", "Tool execution failed")
                return ToolResult(
                    success=False,
                    error=error_msg,
                    tool_id=resolved_tool_id,
                    tool_name=resolved_tool_name,
                )

        except (NotFoundError, AuthenticationError, RateLimitError):
            raise
        except Exception as e:
            logger.error(f"Tool execution error: {e}")
            raise ExecutionError(str(e), tool_id=resolved_tool_id)


    async def batch_execute(
        self,
        calls: List[Dict[str, Any]],
    ) -> List[Dict[str, Any]]:
        """Execute multiple tools in a single request.

        Args:
            calls: List of dicts, each with ``tool_id`` and optional ``tool_input``.
                   Maximum 10 calls per batch.

        Returns:
            List of result dicts, each with ``tool_id``, ``success``, ``result``, ``error``.
        """
        if not calls:
            raise ValidationError("At least 1 call required")
        if len(calls) > 10:
            raise ValidationError("Maximum 10 calls per batch")

        formatted = [
            {"tool_id": c["tool_id"], "tool_input": c.get("tool_input", c.get("parameters", {}))}
            for c in calls
        ]

        data = await self._client._http.post(
            "/v1/tools/call/batch",
            json={"calls": formatted},
        )

        return data.get("results", [])


class SkillsResource:
    """Operations on skills."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def search(
        self,
        query: str,
        limit: int = 10,
    ) -> List[Skill]:
        """Search for skills using semantic search.

        Skills are reusable instructions that teach AI agents
        how to perform specific tasks.

        Args:
            query: Search query.
            limit: Maximum results (default 10).

        Returns:
            List of matching Skill objects (summaries).
        """
        data = await self._client._http.get(
            "/v1/skills/search",
            params={"query": query, "limit": limit},
        )

        # Handle both list and dict responses
        skills_data = data if isinstance(data, list) else data.get("skills", [])

        return [Skill.from_api_response(s) for s in skills_data[:limit]]

    async def get(
        self,
        skill_id: Optional[str] = None,
        skill_name: Optional[str] = None,
    ) -> SkillContent:
        """Get a skill with full content.

        Args:
            skill_id: Skill UUID (preferred).
            skill_name: Skill name.

        Returns:
            Full SkillContent including SKILL.md, scripts, etc.

        Raises:
            ValidationError: If neither skill_id nor skill_name provided.
            NotFoundError: If skill cannot be found.
        """
        if not skill_id and not skill_name:
            raise ValidationError("Either skill_id or skill_name must be provided")

        resolved_skill_id = skill_id

        # If no ID, search by name
        if not resolved_skill_id and skill_name:
            skills = await self.search(query=skill_name, limit=5)

            if not skills:
                raise NotFoundError("Skill", skill_name)

            # Find exact match or use first result
            skill = None
            for s in skills:
                if s.name == skill_name:
                    skill = s
                    break

            if not skill:
                skill = skills[0]

            resolved_skill_id = skill.id

        if not resolved_skill_id:
            raise NotFoundError("Skill", skill_id or skill_name or "")

        data = await self._client._http.get(f"/v1/skills/{resolved_skill_id}")
        return SkillContent.from_api_response(data)


class IdentityResource:
    """Operations on user identity."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client
        self._cached_identity: Optional[Identity] = None

    async def get(self) -> Identity:
        """Get the current user's identity.

        Returns:
            User identity with profile, contacts, preferences.
        """
        data = await self._client._http.get("/v1/identity/api")
        self._cached_identity = Identity.from_api_response(data)
        return self._cached_identity



class WorkflowsResource:
    """Operations on workflows."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def list(
        self,
        query: str = "",
        limit: int = 10,
    ) -> List[Workflow]:
        """List public workflows.

        Args:
            query: Optional search query to filter by name/description.
            limit: Maximum number of results (default 10).

        Returns:
            List of Workflow objects.
        """
        params: Dict[str, Any] = {"limit": limit}
        if query:
            params["search"] = query
        data = await self._client._http.get(
            "/v1/workflows/public",
            params=params,
        )
        items = data if isinstance(data, list) else data.get("workflows", [])
        return [Workflow.from_api_response(w) for w in items[:limit]]

    async def get(self, workflow_id: str) -> Workflow:
        """Get a workflow by ID.

        Args:
            workflow_id: The workflow UUID.

        Returns:
            Workflow details.
        """
        data = await self._client._http.get(f"/v1/workflows/{workflow_id}")
        return Workflow.from_api_response(data)

    async def execute(
        self,
        workflow_id: str,
        inputs: Optional[Dict[str, Any]] = None,
    ) -> WorkflowExecution:
        """Execute a workflow.

        Args:
            workflow_id: The workflow UUID.
            inputs: Optional dict of input values.

        Returns:
            WorkflowExecution with step results.
        """
        data = await self._client._http.post(
            f"/v1/workflows/{workflow_id}/execute",
            json={"inputs": inputs or {}},
        )
        return WorkflowExecution.from_api_response(data)

    async def get_execution(self, execution_id: str) -> WorkflowExecution:
        """Get a workflow execution result.

        Args:
            execution_id: The execution UUID.

        Returns:
            WorkflowExecution with full results.
        """
        data = await self._client._http.get(
            f"/v1/workflows/executions/{execution_id}",
        )
        return WorkflowExecution.from_api_response(data)

    async def create(
        self,
        name: str,
        steps: List[Dict[str, Any]],
        description: str = "",
        visibility: str = "private",
        tags: Optional[List[str]] = None,
    ) -> Workflow:
        """Create a new workflow.

        Args:
            name: Workflow name.
            steps: List of step dicts with ``step_number``, ``tool_id``, optional ``tool_name``,
                   ``description``, ``input_mapping``.
            description: Optional workflow description.
            visibility: ``"private"`` or ``"public"`` (default ``"private"``).
            tags: Optional list of tags.

        Returns:
            The created Workflow.
        """
        payload: Dict[str, Any] = {
            "name": name,
            "steps": steps,
            "description": description,
            "visibility": visibility,
        }
        if tags:
            payload["tags"] = tags

        data = await self._client._http.post("/v1/workflows", json=payload)
        return Workflow.from_api_response(data)

    async def update(
        self,
        workflow_id: str,
        **updates: Any,
    ) -> Workflow:
        """Update a workflow (owner only).

        Args:
            workflow_id: The workflow UUID.
            **updates: Fields to update (name, description, steps, visibility, tags).

        Returns:
            The updated Workflow.
        """
        data = await self._client._http.patch(
            f"/v1/workflows/{workflow_id}",
            json=updates,
        )
        return Workflow.from_api_response(data)

    async def delete(self, workflow_id: str) -> bool:
        """Delete a workflow (owner only).

        Args:
            workflow_id: The workflow UUID.

        Returns:
            True on success.
        """
        await self._client._http.delete(f"/v1/workflows/{workflow_id}")
        return True


class SitesResource:
    """Operations on Agent Web sites.

    Agent Web crawls websites across multiple pages and extracts rich structured
    data (identity, products, team, blog, jobs, contact, about, services, docs,
    pricing, faq, legal, navigation) for AI agent consumption.
    """

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def search(
        self,
        query: str = "",
        category: Optional[str] = None,
        limit: int = 10,
    ) -> List[AgentSiteListItem]:
        """Search the Agent Web directory.

        Args:
            query: Search query to filter sites.
            category: Optional category filter.
            limit: Maximum number of results (default 10).

        Returns:
            List of AgentSiteListItem objects.
        """
        params: Dict[str, Any] = {"limit": limit}
        if query:
            params["search"] = query
        if category:
            params["category"] = category
        endpoint = "/v1/agent-sites/search" if query else "/v1/agent-sites/directory"
        data = await self._client._http.get(endpoint, params=params)
        items = data if isinstance(data, list) else data.get("sites", [])
        return [AgentSiteListItem.from_api_response(s) for s in items[:limit]]

    async def get(self, site_id: str) -> AgentSite:
        """Get a site by ID.

        Args:
            site_id: The site UUID.

        Returns:
            Full AgentSite with components.
        """
        data = await self._client._http.get(f"/v1/agent-sites/{site_id}")
        return AgentSite.from_api_response(data)

    async def get_by_domain(self, domain: str) -> AgentSite:
        """Get a site by domain.

        Args:
            domain: The domain (e.g. 'stripe.com').

        Returns:
            Full AgentSite with components.
        """
        data = await self._client._http.get(f"/v1/agent-sites/domain/{domain}")
        return AgentSite.from_api_response(data)


class WalletResource:
    """Operations on wallet balance and transactions."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def get_balance(self) -> WalletBalance:
        """Get the current user's wallet balance.

        Returns:
            WalletBalance with balance in cents and dollars.
        """
        data = await self._client._http.get("/v1/wallet/balance")
        return WalletBalance.from_api_response(data)

    async def get_transactions(
        self,
        limit: int = 50,
        offset: int = 0,
    ) -> List[WalletTransaction]:
        """Get wallet transaction history.

        Args:
            limit: Maximum number of transactions (default 50, max 100).
            offset: Number of transactions to skip for pagination.

        Returns:
            List of WalletTransaction objects.
        """
        data = await self._client._http.get(
            "/v1/wallet/transactions",
            params={"limit": min(limit, 100), "offset": offset},
        )
        items = data.get("transactions", []) if isinstance(data, dict) else data
        return [WalletTransaction.from_api_response(t) for t in items]

    async def get_spending_limits(self) -> SpendingLimits:
        """Get the current user's USDC spending limits.

        Returns:
            SpendingLimits with max_per_call_atomic and daily_limit_atomic.
        """
        data = await self._client._http.get("/v1/x402/settings")
        return SpendingLimits.from_api_response(data)

    async def update_spending_limits(
        self,
        max_per_call_atomic: Optional[int] = None,
        daily_limit_atomic: Optional[int] = None,
    ) -> SpendingLimits:
        """Update the current user's USDC spending limits.

        Args:
            max_per_call_atomic: Max USDC per tool call in atomic units (0 to 5,000,000).
            daily_limit_atomic: Daily spending limit in atomic units. None to remove.

        Returns:
            Updated SpendingLimits.
        """
        payload: Dict[str, Any] = {}
        if max_per_call_atomic is not None:
            payload["max_per_call_atomic"] = max_per_call_atomic
        if daily_limit_atomic is not None:
            payload["daily_limit_atomic"] = daily_limit_atomic
        data = await self._client._http.put("/v1/x402/settings", json=payload)
        return SpendingLimits.from_api_response(data)


class RatingsResource:
    """Operations on tool ratings."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def submit(
        self,
        tool_id: str,
        rating: int,
        comment: Optional[str] = None,
    ) -> ToolRating:
        """Submit or update a rating for a tool.

        Args:
            tool_id: The tool UUID.
            rating: Rating from 1 to 5.
            comment: Optional text comment.

        Returns:
            The submitted ToolRating.
        """
        payload: Dict[str, Any] = {"tool_id": tool_id, "rating": rating}
        if comment is not None:
            payload["comment"] = comment
        data = await self._client._http.post("/v1/ratings", json=payload)
        return ToolRating.from_api_response(data)

    async def get_mine(self, tool_id: str) -> Optional[ToolRating]:
        """Get the current user's rating for a tool.

        Args:
            tool_id: The tool UUID.

        Returns:
            ToolRating if the user has rated, None otherwise.
        """
        data = await self._client._http.get(f"/v1/ratings/my/{tool_id}")
        if isinstance(data, dict) and data.get("rating") is None:
            return None
        return ToolRating.from_api_response(data)

    async def get_tool_ratings(self, tool_id: str) -> RatingAggregate:
        """Get aggregated ratings for a tool (public).

        Args:
            tool_id: The tool UUID.

        Returns:
            RatingAggregate with average, count, and distribution.
        """
        data = await self._client._http.get(f"/v1/ratings/tool/{tool_id}")
        return RatingAggregate.from_api_response(data)


class CredentialsResource:
    """Operations on credential storage."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def store(
        self,
        service_id: str,
        credential_type: str,
        credential_value: str,
    ) -> CredentialStoreResult:
        """Store a credential for a service.

        Only supports 'bearer' and 'api_key' credential types.
        OAuth credentials require the browser flow.

        Args:
            service_id: The service UUID.
            credential_type: Either 'bearer' or 'api_key'.
            credential_value: The credential value (token or key).

        Returns:
            CredentialStoreResult confirming storage.
        """
        data = await self._client._http.post(
            "/v1/credentials/store",
            json={
                "service_id": service_id,
                "credential_type": credential_type,
                "credential_value": credential_value,
            },
        )
        return CredentialStoreResult.from_api_response(data)


class DeviceAuthResource:
    """Operations for device code authentication flow."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def request_code(
        self,
        client_name: str = "Python SDK",
    ) -> DeviceCode:
        """Start a device authorization flow.

        The returned user_code should be displayed to the user,
        who enters it at the verification_url to authorize.

        Args:
            client_name: Name of the client requesting auth.

        Returns:
            DeviceCode with device_code (for polling) and user_code.
        """
        data = await self._client._http.post(
            "/v1/auth/device/code",
            json={"client_name": client_name},
        )
        return DeviceCode.from_api_response(data)

    async def poll_token(self, device_code: str) -> DeviceToken:
        """Poll for a device token after user authorization.

        Call this repeatedly (every `interval` seconds from DeviceCode)
        until it returns a token or raises an error.

        Args:
            device_code: The device_code from request_code().

        Returns:
            DeviceToken with api_key on success.

        Raises:
            AuthorizationError: If authorization is still pending (428).
            NotFoundError: If the device code has expired (410).
        """
        data = await self._client._http.post(
            "/v1/auth/device/token",
            json={"device_code": device_code},
        )
        return DeviceToken.from_api_response(data)


class APIKeysResource:
    """Operations on API keys."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def list(self) -> List[APIKeyResponse]:
        """List all active API keys for the current user.

        Returns:
            List of APIKeyResponse objects.
        """
        data = await self._client._http.get("/v1/api-keys")
        items = data if isinstance(data, list) else []
        return [APIKeyResponse.from_api_response(k) for k in items]

    async def create(
        self,
        name: str,
        permissions: Optional[Dict[str, Any]] = None,
    ) -> APIKeyWithSecret:
        """Create a new API key.

        Args:
            name: Display name for the key.
            permissions: Optional dict with allowed_services, allowed_tools,
                         max_spend_per_call_cents, max_spend_per_day_cents.

        Returns:
            APIKeyWithSecret with the plaintext key (shown only once).
        """
        payload: Dict[str, Any] = {"name": name}
        if permissions is not None:
            payload["permissions"] = permissions
        data = await self._client._http.post("/v1/api-keys", json=payload)
        return APIKeyWithSecret.from_api_response(data)

    async def rotate(self, key_id: str) -> APIKeyWithSecret:
        """Rotate an API key, generating new key material.

        The old key is immediately invalidated.

        Args:
            key_id: The API key UUID.

        Returns:
            APIKeyWithSecret with the new plaintext key.
        """
        data = await self._client._http.post(f"/v1/api-keys/{key_id}/rotate")
        return APIKeyWithSecret.from_api_response(data)

    async def revoke(self, key_id: str) -> bool:
        """Revoke (soft-delete) an API key.

        Args:
            key_id: The API key UUID.

        Returns:
            True on success.
        """
        await self._client._http.delete(f"/v1/api-keys/{key_id}")
        return True


class WebhooksResource:
    """Operations on webhooks."""

    def __init__(self, client: "AsyncDanubeClient"):
        self._client = client

    async def list(self) -> List[WebhookResponse]:
        """List all webhooks for the current user.

        Returns:
            List of WebhookResponse objects.
        """
        data = await self._client._http.get("/v1/webhooks")
        items = data if isinstance(data, list) else []
        return [WebhookResponse.from_api_response(w) for w in items]

    async def create(
        self,
        url: str,
        events: List[str],
        description: Optional[str] = None,
    ) -> WebhookCreateResponse:
        """Create a new webhook.

        Args:
            url: The HTTPS endpoint URL.
            events: List of event types to subscribe to.
            description: Optional description.

        Returns:
            WebhookCreateResponse with the signing secret (shown only once).
        """
        payload: Dict[str, Any] = {"url": url, "events": events}
        if description is not None:
            payload["description"] = description
        data = await self._client._http.post("/v1/webhooks", json=payload)
        return WebhookCreateResponse.from_api_response(data)

    async def update(
        self,
        webhook_id: str,
        url: Optional[str] = None,
        events: Optional[List[str]] = None,
        is_active: Optional[bool] = None,
        description: Optional[str] = None,
    ) -> WebhookResponse:
        """Update a webhook's URL, events, or active status.

        Args:
            webhook_id: The webhook UUID.
            url: New HTTPS endpoint URL.
            events: New list of event types.
            is_active: Enable or disable the webhook.
            description: New description.

        Returns:
            Updated WebhookResponse.
        """
        payload: Dict[str, Any] = {}
        if url is not None:
            payload["url"] = url
        if events is not None:
            payload["events"] = events
        if is_active is not None:
            payload["is_active"] = is_active
        if description is not None:
            payload["description"] = description
        data = await self._client._http.patch(f"/v1/webhooks/{webhook_id}", json=payload)
        return WebhookResponse.from_api_response(data)

    async def delete(self, webhook_id: str) -> bool:
        """Delete a webhook.

        Args:
            webhook_id: The webhook UUID.

        Returns:
            True on success.
        """
        await self._client._http.delete(f"/v1/webhooks/{webhook_id}")
        return True

    async def get_deliveries(
        self,
        webhook_id: str,
        limit: int = 20,
    ) -> List[WebhookDeliveryResponse]:
        """Get recent deliveries for a webhook.

        Args:
            webhook_id: The webhook UUID.
            limit: Maximum number of deliveries (default 20, max 100).

        Returns:
            List of WebhookDeliveryResponse objects.
        """
        data = await self._client._http.get(
            f"/v1/webhooks/{webhook_id}/deliveries",
            params={"limit": min(limit, 100)},
        )
        items = data if isinstance(data, list) else []
        return [WebhookDeliveryResponse.from_api_response(d) for d in items]


class AsyncDanubeClient:
    """Async client for the Danube AI API.

    Example:
        async with AsyncDanubeClient(api_key="dk_...") as client:
            services = await client.services.list()
            result = await client.tools.execute("tool-id", parameters={"key": "value"})
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: float = 30.0,
        max_retries: int = 3,
    ):
        """Initialize the Danube async client.

        Args:
            api_key: Danube API key. Falls back to DANUBE_API_KEY env var.
            base_url: API base URL. Defaults to https://api.danubeai.com
            timeout: Request timeout in seconds.
            max_retries: Number of retries for failed requests.
        """
        # Build config with overrides
        config = DanubeConfig()
        if api_key:
            config.api_key = api_key
        if base_url:
            config.base_url = base_url.rstrip("/")
        config.timeout = timeout
        config.max_retries = max_retries

        # Validate configuration
        config.validate()

        self._config = config
        self._http = HTTPClient(config)

        # Initialize resource handlers (lazy)
        self._agents: Optional[AgentsResource] = None
        self._services: Optional[ServicesResource] = None
        self._tools: Optional[ToolsResource] = None
        self._skills: Optional[SkillsResource] = None
        self._identity: Optional[IdentityResource] = None
        self._workflows: Optional[WorkflowsResource] = None
        self._sites: Optional[SitesResource] = None
        self._wallet: Optional[WalletResource] = None
        self._ratings: Optional[RatingsResource] = None
        self._credentials: Optional[CredentialsResource] = None
        self._device_auth: Optional[DeviceAuthResource] = None
        self._api_keys: Optional[APIKeysResource] = None
        self._webhooks: Optional[WebhooksResource] = None

    @property
    def agents(self) -> AgentsResource:
        """Access agents resource."""
        if self._agents is None:
            self._agents = AgentsResource(self)
        return self._agents

    @property
    def services(self) -> ServicesResource:
        """Access services resource."""
        if self._services is None:
            self._services = ServicesResource(self)
        return self._services

    @property
    def tools(self) -> ToolsResource:
        """Access tools resource."""
        if self._tools is None:
            self._tools = ToolsResource(self)
        return self._tools

    @property
    def skills(self) -> SkillsResource:
        """Access skills resource."""
        if self._skills is None:
            self._skills = SkillsResource(self)
        return self._skills

    @property
    def identity(self) -> IdentityResource:
        """Access identity resource."""
        if self._identity is None:
            self._identity = IdentityResource(self)
        return self._identity

    @property
    def workflows(self) -> WorkflowsResource:
        """Access workflows resource."""
        if self._workflows is None:
            self._workflows = WorkflowsResource(self)
        return self._workflows

    @property
    def sites(self) -> SitesResource:
        """Access agent-friendly sites resource."""
        if self._sites is None:
            self._sites = SitesResource(self)
        return self._sites

    @property
    def wallet(self) -> WalletResource:
        """Access wallet resource."""
        if self._wallet is None:
            self._wallet = WalletResource(self)
        return self._wallet

    @property
    def ratings(self) -> RatingsResource:
        """Access ratings resource."""
        if self._ratings is None:
            self._ratings = RatingsResource(self)
        return self._ratings

    @property
    def credentials(self) -> CredentialsResource:
        """Access credentials resource."""
        if self._credentials is None:
            self._credentials = CredentialsResource(self)
        return self._credentials

    @property
    def device_auth(self) -> DeviceAuthResource:
        """Access device auth resource."""
        if self._device_auth is None:
            self._device_auth = DeviceAuthResource(self)
        return self._device_auth

    @property
    def api_keys(self) -> APIKeysResource:
        """Access API keys resource."""
        if self._api_keys is None:
            self._api_keys = APIKeysResource(self)
        return self._api_keys

    @property
    def webhooks(self) -> WebhooksResource:
        """Access webhooks resource."""
        if self._webhooks is None:
            self._webhooks = WebhooksResource(self)
        return self._webhooks

    async def verify_connection(self) -> bool:
        """Verify the connection to the Danube API.

        Returns:
            True if connection is successful and API key is valid.
        """
        try:
            await self._http.get("/v1/services/public")
            return True
        except Exception as e:
            logger.warning(f"Connection verification failed: {e}")
            return False

    async def close(self) -> None:
        """Close the client and release resources."""
        await self._http.close()

    async def __aenter__(self) -> "AsyncDanubeClient":
        """Async context manager entry."""
        return self

    async def __aexit__(self, *args: Any) -> None:
        """Async context manager exit."""
        await self.close()
