"""
Asynchronous AgentPhone client (requires httpx).
"""

from __future__ import annotations

from typing import Optional, TYPE_CHECKING

if TYPE_CHECKING:
    import httpx

from ._http import DEFAULT_BASE_URL, _raise_for_status
from .models import (
    Agent,
    AgentList,
    Call,
    CallList,
    Contact,
    ContactList,
    Conversation,
    ConversationList,
    DailyUsage,
    MessageList,
    MonthlyUsage,
    PhoneNumber,
    PhoneNumberList,
    Usage,
    Webhook,
    WebhookDelivery,
    WebhookDeliveryStats,
)


class _AsyncResource:
    def __init__(self, client: "AsyncAgentPhone") -> None:
        self._client = client

    async def _get(self, path: str, **params) -> dict:
        return await self._client._request("GET", path, params=params or None)

    async def _post(self, path: str, **body) -> dict:
        return await self._client._request(
            "POST", path, json={k: v for k, v in body.items() if v is not None}
        )

    async def _patch(self, path: str, **body) -> dict:
        return await self._client._request(
            "PATCH", path, json={k: v for k, v in body.items() if v is not None}
        )

    async def _delete(self, path: str) -> dict:
        return await self._client._request("DELETE", path)


class AsyncNumbersResource(_AsyncResource):
    async def list(self, limit: int = 20, offset: int = 0) -> PhoneNumberList:
        return PhoneNumberList.from_dict(await self._get("/v1/numbers", limit=limit, offset=offset))

    async def buy(self, country: str = "US", area_code: Optional[str] = None, agent_id: Optional[str] = None) -> PhoneNumber:
        return PhoneNumber.from_dict(await self._post("/v1/numbers", country=country, areaCode=area_code, agentId=agent_id))

    async def release(self, number_id: str) -> dict:
        return await self._delete(f"/v1/numbers/{number_id}")

    async def get_messages(
        self,
        number_id: str,
        limit: int = 50,
        before: Optional[str] = None,
        after: Optional[str] = None,
    ) -> MessageList:
        params: dict = {"limit": limit}
        if before:
            params["before"] = before
        if after:
            params["after"] = after
        return MessageList.from_dict(await self._get(f"/v1/numbers/{number_id}/messages", **params))

    async def list_calls(self, number_id: str, limit: int = 20, offset: int = 0) -> CallList:
        return CallList.from_dict(await self._get(f"/v1/numbers/{number_id}/calls", limit=limit, offset=offset))


class AsyncContactsResource(_AsyncResource):
    async def list(self, limit: int = 50, offset: int = 0, search: Optional[str] = None) -> ContactList:
        params: dict = {"limit": limit, "offset": offset}
        if search:
            params["search"] = search
        return ContactList.from_dict(await self._get("/v1/contacts", **params))

    async def create(
        self,
        phone_number: str,
        name: str,
        email: Optional[str] = None,
        notes: Optional[str] = None,
    ) -> Contact:
        return Contact.from_dict(
            await self._post("/v1/contacts", phoneNumber=phone_number, name=name, email=email, notes=notes)
        )

    async def get(self, contact_id: str) -> Contact:
        return Contact.from_dict(await self._get(f"/v1/contacts/{contact_id}"))

    async def update(
        self,
        contact_id: str,
        phone_number: Optional[str] = None,
        name: Optional[str] = None,
        email: Optional[str] = None,
        notes: Optional[str] = None,
    ) -> Contact:
        return Contact.from_dict(
            await self._patch(f"/v1/contacts/{contact_id}", phoneNumber=phone_number, name=name, email=email, notes=notes)
        )

    async def delete(self, contact_id: str) -> dict:
        return await self._delete(f"/v1/contacts/{contact_id}")


class AsyncAgentsResource(_AsyncResource):
    async def list(self, limit: int = 20, offset: int = 0) -> AgentList:
        return AgentList.from_dict(await self._get("/v1/agents", limit=limit, offset=offset))

    async def create(
        self,
        name: str,
        description: Optional[str] = None,
        voice_mode: Optional[str] = None,
        system_prompt: Optional[str] = None,
        begin_message: Optional[str] = None,
        voice: Optional[str] = None,
        transfer_number: Optional[str] = None,
        voicemail_message: Optional[str] = None,
    ) -> Agent:
        return Agent.from_dict(
            await self._post(
                "/v1/agents",
                name=name,
                description=description,
                voiceMode=voice_mode,
                systemPrompt=system_prompt,
                beginMessage=begin_message,
                voice=voice,
                transferNumber=transfer_number,
                voicemailMessage=voicemail_message,
            )
        )

    async def get(self, agent_id: str) -> Agent:
        return Agent.from_dict(await self._get(f"/v1/agents/{agent_id}"))

    async def update(
        self,
        agent_id: str,
        name: Optional[str] = None,
        description: Optional[str] = None,
        voice_mode: Optional[str] = None,
        system_prompt: Optional[str] = None,
        begin_message: Optional[str] = None,
        voice: Optional[str] = None,
        transfer_number: Optional[str] = None,
        voicemail_message: Optional[str] = None,
    ) -> Agent:
        return Agent.from_dict(
            await self._patch(
                f"/v1/agents/{agent_id}",
                name=name,
                description=description,
                voiceMode=voice_mode,
                systemPrompt=system_prompt,
                beginMessage=begin_message,
                voice=voice,
                transferNumber=transfer_number,
                voicemailMessage=voicemail_message,
            )
        )

    async def delete(self, agent_id: str) -> dict:
        return await self._delete(f"/v1/agents/{agent_id}")

    async def attach_number(self, agent_id: str, number_id: str) -> dict:
        return await self._post(f"/v1/agents/{agent_id}/numbers", numberId=number_id)

    async def detach_number(self, agent_id: str, number_id: str) -> dict:
        """Detach a phone number from an agent. The number remains active and can be re-attached."""
        return await self._delete(f"/v1/agents/{agent_id}/numbers/{number_id}")

    async def list_calls(self, agent_id: str, limit: int = 20, offset: int = 0) -> CallList:
        return CallList.from_dict(await self._get(f"/v1/agents/{agent_id}/calls", limit=limit, offset=offset))

    async def list_conversations(self, agent_id: str, limit: int = 20, offset: int = 0) -> ConversationList:
        return ConversationList.from_dict(await self._get(f"/v1/agents/{agent_id}/conversations", limit=limit, offset=offset))

    async def list_voices(self) -> list[dict]:
        """List available voices for agents."""
        data = await self._get("/v1/agents/voices")
        return data.get("data", data if isinstance(data, list) else [])

    async def set_webhook(self, agent_id: str, url: str, context_limit: Optional[int] = None, timeout: Optional[int] = None) -> Webhook:
        return Webhook.from_dict(await self._post(f"/v1/agents/{agent_id}/webhook", url=url, contextLimit=context_limit, timeout=timeout))

    async def get_webhook(self, agent_id: str) -> Optional[Webhook]:
        data = await self._get(f"/v1/agents/{agent_id}/webhook")
        return Webhook.from_dict(data) if data else None

    async def delete_webhook(self, agent_id: str) -> dict:
        return await self._delete(f"/v1/agents/{agent_id}/webhook")

    async def list_webhook_deliveries(self, agent_id: str, limit: int = 50) -> list[WebhookDelivery]:
        data = await self._get(f"/v1/agents/{agent_id}/webhook/deliveries", limit=limit)
        return [WebhookDelivery.from_dict(d) for d in data]

    async def test_webhook(self, agent_id: str) -> dict:
        return await self._post(f"/v1/agents/{agent_id}/webhook/test")


class AsyncCallsResource(_AsyncResource):
    async def list(
        self,
        limit: int = 20,
        offset: int = 0,
        status: Optional[str] = None,
        direction: Optional[str] = None,
        type: Optional[str] = None,
        search: Optional[str] = None,
    ) -> CallList:
        params: dict = {"limit": limit, "offset": offset}
        if status:
            params["status"] = status
        if direction:
            params["direction"] = direction
        if type:
            params["type"] = type
        if search:
            params["search"] = search
        return CallList.from_dict(await self._get("/v1/calls", **params))

    async def get(self, call_id: str) -> Call:
        return Call.from_dict(await self._get(f"/v1/calls/{call_id}"))

    async def make(
        self,
        agent_id: str,
        to_number: str,
        initial_greeting: Optional[str] = None,
        from_number_id: Optional[str] = None,
        voice: Optional[str] = None,
    ) -> Call:
        return Call.from_dict(
            await self._post(
                "/v1/calls",
                agentId=agent_id,
                toNumber=to_number,
                initialGreeting=initial_greeting,
                fromNumberId=from_number_id,
                voice=voice,
            )
        )

    async def make_conversation(
        self,
        agent_id: str,
        to_number: str,
        topic: str,
        initial_greeting: Optional[str] = None,
        from_number_id: Optional[str] = None,
        voice: Optional[str] = None,
    ) -> Call:
        return Call.from_dict(
            await self._post(
                "/v1/calls",
                agentId=agent_id,
                toNumber=to_number,
                systemPrompt=topic,
                initialGreeting=initial_greeting,
                fromNumberId=from_number_id,
                voice=voice,
            )
        )

    async def create_web_call(self, agent_id: str, metadata: Optional[dict] = None) -> dict:
        return await self._post("/v1/calls/web", agentId=agent_id, metadata=metadata)

    async def get_transcript(self, call_id: str) -> dict:
        return await self._get(f"/v1/calls/{call_id}/transcript")


class AsyncMessagesResource(_AsyncResource):
    async def send(
        self,
        agent_id: str,
        to_number: str,
        body: str,
        media_url: Optional[str] = None,
        number_id: Optional[str] = None,
    ) -> dict:
        """Send an outbound SMS or iMessage."""
        return await self._post(
            "/v1/messages",
            agent_id=agent_id,
            to_number=to_number,
            body=body,
            media_url=media_url,
            number_id=number_id,
        )

    async def react(self, message_id: str, reaction: str) -> dict:
        """Send a tapback reaction to a message (iMessage only)."""
        return await self._post(f"/v1/messages/{message_id}/reactions", reaction=reaction)


class AsyncConversationsResource(_AsyncResource):
    async def list(self, limit: int = 20, offset: int = 0) -> ConversationList:
        return ConversationList.from_dict(await self._get("/v1/conversations", limit=limit, offset=offset))

    async def get(self, conversation_id: str, message_limit: int = 50) -> Conversation:
        return Conversation.from_dict(
            await self._get(f"/v1/conversations/{conversation_id}", message_limit=message_limit)
        )

    async def update(self, conversation_id: str, metadata: Optional[dict] = None) -> Conversation:
        return Conversation.from_dict(await self._patch(f"/v1/conversations/{conversation_id}", metadata=metadata))

    async def get_messages(
        self,
        conversation_id: str,
        limit: int = 50,
        before: Optional[str] = None,
        after: Optional[str] = None,
    ) -> MessageList:
        params: dict = {"limit": limit}
        if before:
            params["before"] = before
        if after:
            params["after"] = after
        return MessageList.from_dict(await self._get(f"/v1/conversations/{conversation_id}/messages", **params))


class AsyncWebhooksResource(_AsyncResource):
    async def get(self) -> Optional[Webhook]:
        data = await self._get("/v1/webhooks")
        return Webhook.from_dict(data) if data else None

    async def set(self, url: str, context_limit: Optional[int] = None, timeout: Optional[int] = None) -> Webhook:
        return Webhook.from_dict(await self._post("/v1/webhooks", url=url, contextLimit=context_limit, timeout=timeout))

    async def delete(self) -> dict:
        return await self._delete("/v1/webhooks")

    async def list_deliveries(self, limit: int = 50) -> list[WebhookDelivery]:
        data = await self._get("/v1/webhooks/deliveries", limit=limit)
        return [WebhookDelivery.from_dict(d) for d in data]

    async def test(self) -> dict:
        return await self._post("/v1/webhooks/test")

    async def get_delivery_stats(self, hours: int = 24) -> WebhookDeliveryStats:
        return WebhookDeliveryStats.from_dict(await self._get("/v1/webhooks/deliveries/stats", hours=hours))

    async def get_all_time_stats(self) -> dict:
        return await self._get("/v1/webhooks/deliveries/all-time")


class AsyncUsageResource(_AsyncResource):
    async def get(self) -> Usage:
        return Usage.from_dict(await self._get("/v1/usage"))

    async def get_daily(self, days: int = 30) -> DailyUsage:
        return DailyUsage.from_dict(await self._get("/v1/usage/daily", days=days))

    async def get_monthly(self, months: int = 6) -> MonthlyUsage:
        return MonthlyUsage.from_dict(await self._get("/v1/usage/monthly", months=months))


class AsyncAgentPhone:
    """
    Asynchronous AgentPhone client.

    Requires: pip install agentphone[async]

    Example::

        import os
        from agentphone import AsyncAgentPhone

        async with AsyncAgentPhone(api_key=os.environ["AGENTPHONE_API_KEY"]) as client:
            number = await client.numbers.buy(country="US")
            call = await client.calls.make_conversation(
                agent_id="...",
                to_number="+14155551234",
                topic="Schedule a dentist appointment for next Tuesday at 2pm",
            )
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = 30.0,
    ) -> None:
        try:
            import httpx as _httpx
        except ImportError:
            raise ImportError(
                "The async client requires httpx. Install it with: pip install agentphone[async]"
            )
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._client = _httpx.AsyncClient(
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "User-Agent": "agentphone-python/0.4.0",
            },
            timeout=timeout,
        )

        self.numbers = AsyncNumbersResource(self)
        self.contacts = AsyncContactsResource(self)
        self.agents = AsyncAgentsResource(self)
        self.calls = AsyncCallsResource(self)
        self.messages = AsyncMessagesResource(self)
        self.conversations = AsyncConversationsResource(self)
        self.webhooks = AsyncWebhooksResource(self)
        self.usage = AsyncUsageResource(self)

    async def _request(self, method: str, path: str, **kwargs) -> dict:  # type: ignore[override]
        url = f"{self.base_url}{path}"
        resp = await self._client.request(method, url, **kwargs)

        if not resp.is_success:
            _raise_for_status(resp.status_code, resp.text)

        if resp.status_code == 204:
            return {}
        return resp.json()

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

    async def __aenter__(self) -> "AsyncAgentPhone":
        return self

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