"""Async Commune Python SDK client — mirrors the sync client API surface."""

from __future__ import annotations

import os
from typing import Any, Literal, Optional
from urllib.parse import quote

from commune._async_http import AsyncHttpClient
from commune.types import (
    Attachment,
    DeleteResult,
    DomainDnsRecord,
    DomainVerificationResult,
    CreateDomainPayload,
    CreateInboxPayload,
    UpdateInboxPayload,
    SetInboxWebhookPayload,
    SendMessagePayload,
    SendMessageResult,
    UploadAttachmentPayload,
    AttachmentUpload,
    AttachmentUrl,
    Domain,
    Inbox,
    Message,
    Thread,
    ThreadList,
    SearchThreadResult,
    ThreadMetadataEntry,
    DeliveryEventEntry,
    DeliveryMetricsResult,
    SuppressionEntry,
    ExtractionSchemaPayload,
    PhoneNumber,
    UpdatePhoneNumberPayload,
    PhoneSettings,
    SmsConversation,
    SmsMessage,
    SendSmsPayload,
    SendSmsResult,
    CreditBalance,
    CreditBundle,
)


class _AsyncDomains:
    """Async domain management."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def list(self) -> list[Domain]:
        """List all domains in your organization."""
        data = await self._http.get("/v1/domains")
        if isinstance(data, list):
            return [Domain.model_validate(d) for d in data]
        return [Domain.model_validate(d) for d in (data or [])]

    async def create(self, name: str, region: str | None = None) -> Domain:
        """Create a new custom domain."""
        payload = CreateDomainPayload(name=name, region=region)
        data = await self._http.post(
            "/v1/domains",
            json=payload.model_dump(exclude_none=True),
        )
        return Domain.model_validate(data)

    async def get(self, domain_id: str) -> Domain:
        """Get details for a single domain."""
        data = await self._http.get(f"/v1/domains/{quote(domain_id)}")
        return Domain.model_validate(data)

    async def verify(self, domain_id: str) -> DomainVerificationResult:
        """Trigger DNS verification for a domain."""
        data = await self._http.post(f"/v1/domains/{quote(domain_id)}/verify")
        return DomainVerificationResult.model_validate(data)

    async def records(self, domain_id: str) -> list[DomainDnsRecord]:
        """Get DNS records required for domain verification."""
        data = await self._http.get(f"/v1/domains/{quote(domain_id)}/records")
        records = data if isinstance(data, list) else []
        return [DomainDnsRecord.model_validate(record) for record in records]


class _AsyncInboxes:
    """Async inbox management."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def list(self, domain_id: Optional[str] = None) -> list[Inbox]:
        """List inboxes."""
        if domain_id:
            data = await self._http.get(f"/v1/domains/{quote(domain_id)}/inboxes")
        else:
            data = await self._http.get("/v1/inboxes")
        if isinstance(data, list):
            return [Inbox.model_validate(d) for d in data]
        return [Inbox.model_validate(d) for d in (data or [])]

    async def create(
        self,
        local_part: str,
        *,
        domain_id: Optional[str] = None,
        name: Optional[str] = None,
        webhook: Optional[dict[str, Any]] = None,
    ) -> Inbox:
        """Create a new inbox."""
        payload = CreateInboxPayload(
            local_part=local_part,
            domain_id=domain_id,
            name=name,
            webhook=webhook,
        )
        data = await self._http.post(
            "/v1/inboxes",
            json=payload.model_dump(exclude_none=True),
        )
        return Inbox.model_validate(data)

    async def get(self, domain_id: str, inbox_id: str) -> Inbox:
        """Get a single inbox."""
        data = await self._http.get(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}"
        )
        return Inbox.model_validate(data)

    async def update(
        self,
        domain_id: str,
        inbox_id: str,
        *,
        local_part: Optional[str] = None,
        webhook: Optional[dict[str, Any]] = None,
        status: Optional[str] = None,
    ) -> Inbox:
        """Update an inbox."""
        payload = UpdateInboxPayload(
            local_part=local_part,
            webhook=webhook,
            status=status,
        )
        data = await self._http.put(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}",
            json=payload.model_dump(exclude_none=True),
        )
        return Inbox.model_validate(data)

    async def remove(self, domain_id: str, inbox_id: str) -> bool:
        """Delete an inbox."""
        data = await self._http.delete(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}"
        )
        if isinstance(data, dict):
            return DeleteResult.model_validate(data).ok
        return True

    async def set_webhook(
        self,
        domain_id: str,
        inbox_id: str,
        *,
        endpoint: str,
        events: Optional[list[str]] = None,
    ) -> Inbox:
        """Set a webhook on an inbox."""
        payload = SetInboxWebhookPayload(endpoint=endpoint, events=events)
        data = await self._http.put(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}",
            json={"webhook": payload.model_dump(exclude_none=True)},
        )
        return Inbox.model_validate(data)

    async def set_extraction_schema(
        self,
        domain_id: str,
        inbox_id: str,
        *,
        name: str,
        schema: dict[str, Any],
        description: Optional[str] = None,
        enabled: bool = True,
    ) -> Inbox:
        """Set structured extraction schema for an inbox."""
        payload = ExtractionSchemaPayload(
            name=name,
            json_schema=schema,
            description=description,
            enabled=enabled,
        )
        data = await self._http.put(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}/extraction-schema",
            json=payload.model_dump(by_alias=True, exclude_none=True),
        )
        return Inbox.model_validate(data)

    async def remove_extraction_schema(self, domain_id: str, inbox_id: str) -> Inbox:
        """Remove structured extraction schema from an inbox."""
        data = await self._http.delete(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}/extraction-schema"
        )
        return Inbox.model_validate(data)


class _AsyncThreads:
    """Async thread management."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def list(
        self,
        *,
        inbox_id: str | None = None,
        domain_id: str | None = None,
        limit: int = 20,
        cursor: str | None = None,
        order: str = "desc",
    ) -> ThreadList:
        """List email threads with cursor-based pagination."""
        params: dict[str, Any] = {"limit": limit, "order": order}
        if inbox_id:
            params["inbox_id"] = inbox_id
        if domain_id:
            params["domain_id"] = domain_id
        if cursor:
            params["cursor"] = cursor

        raw = await self._http.get("/v1/threads", params=params, unwrap_data=False)
        if isinstance(raw, dict):
            return ThreadList.model_validate(raw)
        if isinstance(raw, list):
            return ThreadList(data=[Thread.model_validate(t) for t in raw])
        return ThreadList()

    async def messages(
        self,
        thread_id: str,
        *,
        limit: int = 50,
        order: str = "asc",
    ) -> list[Message]:
        """Get all messages in a thread."""
        data = await self._http.get(
            f"/v1/threads/{quote(thread_id)}/messages",
            params={"limit": limit, "order": order},
        )
        if isinstance(data, list):
            return [Message.model_validate(m) for m in data]
        return [Message.model_validate(m) for m in (data or [])]

    async def metadata(self, thread_id: str) -> ThreadMetadataEntry:
        """Get triage metadata for a thread."""
        data = await self._http.get(f"/v1/threads/{quote(thread_id)}/metadata")
        return ThreadMetadataEntry.model_validate(data)

    async def set_status(
        self,
        thread_id: str,
        status: Literal["open", "needs_reply", "waiting", "closed"],
    ) -> ThreadMetadataEntry:
        """Set triage status for a thread."""
        data = await self._http.put(
            f"/v1/threads/{quote(thread_id)}/status",
            json={"status": status},
        )
        return ThreadMetadataEntry.model_validate(data)

    async def add_tags(self, thread_id: str, tags: list[str]) -> ThreadMetadataEntry:
        """Add tags to a thread."""
        data = await self._http.post(
            f"/v1/threads/{quote(thread_id)}/tags",
            json={"tags": tags},
        )
        return ThreadMetadataEntry.model_validate(data)

    async def remove_tags(self, thread_id: str, tags: list[str]) -> ThreadMetadataEntry:
        """Remove tags from a thread."""
        data = await self._http.delete(
            f"/v1/threads/{quote(thread_id)}/tags",
            json={"tags": tags},
        )
        return ThreadMetadataEntry.model_validate(data)

    async def assign(self, thread_id: str, assigned_to: Optional[str] = None) -> ThreadMetadataEntry:
        """Assign a thread to an agent/user. Pass None to unassign."""
        data = await self._http.put(
            f"/v1/threads/{quote(thread_id)}/assign",
            json={"assigned_to": assigned_to},
        )
        return ThreadMetadataEntry.model_validate(data)


class _AsyncMessages:
    """Async email sending and listing."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def send(
        self,
        *,
        to: str | list[str],
        subject: str,
        html: str | None = None,
        text: str | None = None,
        from_address: str | None = None,
        cc: list[str] | None = None,
        bcc: list[str] | None = None,
        reply_to: str | None = None,
        thread_id: str | None = None,
        domain_id: str | None = None,
        inbox_id: str | None = None,
        attachments: list[str] | None = None,
        headers: dict[str, str] | None = None,
    ) -> SendMessageResult:
        """Send an email."""
        payload = SendMessagePayload(
            to=to,
            subject=subject,
            html=html,
            text=text,
            from_address=from_address,
            cc=cc,
            bcc=bcc,
            reply_to=reply_to,
            thread_id=thread_id,
            domain_id=domain_id,
            inbox_id=inbox_id,
            attachments=attachments,
            headers=headers,
        )
        data = await self._http.post(
            "/v1/messages/send",
            json=payload.model_dump(by_alias=True, exclude_none=True),
        )
        return SendMessageResult.model_validate(data)

    async def list(
        self,
        *,
        inbox_id: str | None = None,
        domain_id: str | None = None,
        sender: str | None = None,
        limit: int = 50,
        order: str = "desc",
        before: str | None = None,
        after: str | None = None,
    ) -> list[Message]:
        """List messages with filters."""
        params: dict[str, Any] = {"limit": limit, "order": order}
        if inbox_id:
            params["inbox_id"] = inbox_id
        if domain_id:
            params["domain_id"] = domain_id
        if sender:
            params["sender"] = sender
        if before:
            params["before"] = before
        if after:
            params["after"] = after

        data = await self._http.get("/v1/messages", params=params)
        if isinstance(data, list):
            return [Message.model_validate(m) for m in data]
        return [Message.model_validate(m) for m in (data or [])]


class _AsyncAttachments:
    """Async attachment upload and retrieval."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def upload(
        self,
        content: str,
        filename: str,
        mime_type: str,
    ) -> AttachmentUpload:
        """Upload an attachment for later use in emails."""
        payload = UploadAttachmentPayload(
            content=content,
            filename=filename,
            mime_type=mime_type,
        )
        data = await self._http.post(
            "/v1/attachments/upload",
            json=payload.model_dump(exclude_none=True),
        )
        return AttachmentUpload.model_validate(data)

    async def get(self, attachment_id: str) -> Attachment:
        """Get attachment metadata."""
        data = await self._http.get(f"/v1/attachments/{quote(attachment_id)}")
        return Attachment.model_validate(data)

    async def url(self, attachment_id: str, *, expires_in: int = 3600) -> AttachmentUrl:
        """Get a temporary download URL for an attachment."""
        data = await self._http.get(
            f"/v1/attachments/{quote(attachment_id)}/url",
            params={"expires_in": expires_in},
        )
        return AttachmentUrl.model_validate(data)


class _AsyncSearch:
    """Async thread search."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def threads(
        self,
        query: str,
        *,
        inbox_id: str | None = None,
        domain_id: str | None = None,
        limit: int = 20,
    ) -> list[SearchThreadResult]:
        """Search threads by natural language query."""
        params: dict[str, Any] = {"q": query, "limit": limit}
        if inbox_id:
            params["inbox_id"] = inbox_id
        if domain_id:
            params["domain_id"] = domain_id
        data = await self._http.get("/v1/search/threads", params=params)
        if isinstance(data, list):
            return [SearchThreadResult.model_validate(item) for item in data]
        return [SearchThreadResult.model_validate(item) for item in (data or [])]


class _AsyncDelivery:
    """Async delivery metrics, events, and suppressions."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def metrics(
        self,
        *,
        inbox_id: str | None = None,
        domain_id: str | None = None,
        period: str = "7d",
    ) -> DeliveryMetricsResult:
        """Get delivery metrics."""
        data = await self._http.get(
            "/v1/delivery/metrics",
            params={
                "inbox_id": inbox_id,
                "domain_id": domain_id,
                "period": period,
            },
        )
        return DeliveryMetricsResult.model_validate(data)

    async def events(
        self,
        *,
        message_id: str | None = None,
        inbox_id: str | None = None,
        domain_id: str | None = None,
        event_type: str | None = None,
        limit: int = 50,
    ) -> list[DeliveryEventEntry]:
        """Get delivery event log."""
        data = await self._http.get(
            "/v1/delivery/events",
            params={
                "message_id": message_id,
                "inbox_id": inbox_id,
                "domain_id": domain_id,
                "event_type": event_type,
                "limit": limit,
            },
        )
        if isinstance(data, list):
            return [DeliveryEventEntry.model_validate(item) for item in data]
        return [DeliveryEventEntry.model_validate(item) for item in (data or [])]

    async def suppressions(
        self,
        *,
        inbox_id: str | None = None,
        domain_id: str | None = None,
        limit: int = 50,
    ) -> list[SuppressionEntry]:
        """List suppressed recipients."""
        data = await self._http.get(
            "/v1/delivery/suppressions",
            params={
                "inbox_id": inbox_id,
                "domain_id": domain_id,
                "limit": limit,
            },
        )
        if isinstance(data, list):
            return [SuppressionEntry.model_validate(item) for item in data]
        return [SuppressionEntry.model_validate(item) for item in (data or [])]


class _AsyncPhoneNumbers:
    """Async phone number management."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def list(self) -> list[PhoneNumber]:
        """List all active phone numbers in your organization."""
        data = await self._http.get("/v1/phone-numbers")
        if isinstance(data, list):
            return [PhoneNumber.model_validate(d) for d in data]
        return [PhoneNumber.model_validate(d) for d in (data or [])]

    async def get(self, phone_number_id: str) -> PhoneNumber:
        """Get a single phone number."""
        data = await self._http.get(f"/v1/phone-numbers/{quote(phone_number_id)}")
        return PhoneNumber.model_validate(data)

    async def update(
        self,
        phone_number_id: str,
        *,
        friendly_name: Optional[str] = None,
        auto_reply: Optional[str] = None,
        allow_list: Optional[list[str]] = None,
        block_list: Optional[list[str]] = None,
    ) -> PhoneNumber:
        """Update a phone number."""
        payload = UpdatePhoneNumberPayload(
            friendly_name=friendly_name,
            auto_reply=auto_reply,
            allow_list=allow_list,
            block_list=block_list,
        )
        data = await self._http.patch(
            f"/v1/phone-numbers/{quote(phone_number_id)}",
            json=payload.model_dump(exclude_none=True),
        )
        return PhoneNumber.model_validate(data)

    async def settings(self) -> PhoneSettings:
        """Get SMS quota settings for the organization."""
        data = await self._http.get("/v1/phone-settings")
        return PhoneSettings.model_validate(data)


class _AsyncSMS:
    """Async SMS sending and conversations."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def send(
        self,
        *,
        to: str,
        body: str,
        phone_number_id: Optional[str] = None,
        media_url: Optional[list[str]] = None,
    ) -> SendSmsResult:
        """Send an SMS message."""
        payload = SendSmsPayload(
            to=to,
            body=body,
            phone_number_id=phone_number_id,
            media_url=media_url,
        )
        data = await self._http.post(
            "/v1/sms/send",
            json=payload.model_dump(exclude_none=True),
        )
        return SendSmsResult.model_validate(data)

    async def conversations(
        self,
        *,
        phone_number_id: Optional[str] = None,
        limit: int = 20,
        cursor: Optional[str] = None,
    ) -> list[SmsConversation]:
        """List SMS conversations."""
        params: dict[str, Any] = {"limit": limit}
        if phone_number_id:
            params["phone_number_id"] = phone_number_id
        if cursor:
            params["cursor"] = cursor
        data = await self._http.get("/v1/sms/conversations", params=params)
        if isinstance(data, list):
            return [SmsConversation.model_validate(d) for d in data]
        return [SmsConversation.model_validate(d) for d in (data or [])]

    async def thread(
        self,
        remote_number: str,
        *,
        phone_number_id: str,
    ) -> list[SmsMessage]:
        """Get messages in a conversation thread."""
        data = await self._http.get(
            f"/v1/sms/conversations/{quote(remote_number)}",
            params={"phone_number_id": phone_number_id},
        )
        if isinstance(data, list):
            return [SmsMessage.model_validate(m) for m in data]
        return [SmsMessage.model_validate(m) for m in (data or [])]

    async def search(
        self,
        query: str,
        *,
        phone_number_id: Optional[str] = None,
        limit: int = 20,
    ) -> list[SmsMessage]:
        """Semantic search over SMS messages."""
        params: dict[str, Any] = {"q": query, "limit": limit}
        if phone_number_id:
            params["phone_number_id"] = phone_number_id
        data = await self._http.get("/v1/sms/search", params=params)
        if isinstance(data, list):
            return [SmsMessage.model_validate(m) for m in data]
        return [SmsMessage.model_validate(m) for m in (data or [])]


class _AsyncCredits:
    """Async credit balance and bundle management."""

    def __init__(self, http: AsyncHttpClient):
        self._http = http

    async def balance(self) -> CreditBalance:
        """Get the current credit balance for your organization."""
        data = await self._http.get("/v1/credits")
        return CreditBalance.model_validate(data)

    async def bundles(self) -> list[CreditBundle]:
        """List available credit bundles for purchase."""
        data = await self._http.get("/v1/credits/bundles")
        if isinstance(data, list):
            return [CreditBundle.model_validate(d) for d in data]
        return [CreditBundle.model_validate(d) for d in (data or [])]


class AsyncCommuneClient:
    """Async Commune SDK client.

    Identical API to ``CommuneClient`` but all methods are ``async``.

    Example::

        import asyncio
        from commune import AsyncCommuneClient

        async def main():
            async with AsyncCommuneClient(api_key="comm_...") as client:
                inbox = await client.inboxes.create(local_part="support")
                await client.messages.send(
                    to="user@example.com",
                    subject="Hello",
                    html="<p>Hi!</p>",
                    inbox_id=inbox.id,
                )

        asyncio.run(main())

    Args:
        api_key: Your Commune API key (starts with ``comm_``).
            Falls back to the ``COMMUNE_API_KEY`` environment variable.
        base_url: Override the API base URL (default: Commune cloud).
        timeout: Request timeout in seconds (default 30).
    """

    def __init__(
        self,
        api_key: str | None = None,
        *,
        base_url: str | None = None,
        timeout: float = 30.0,
    ):
        resolved_key = api_key or os.environ.get("COMMUNE_API_KEY") or ""
        if not resolved_key:
            raise ValueError(
                "No API key provided. Pass api_key= or set the COMMUNE_API_KEY "
                "environment variable."
            )
        self._http = AsyncHttpClient(api_key=resolved_key, base_url=base_url, timeout=timeout)
        self.domains = _AsyncDomains(self._http)
        self.inboxes = _AsyncInboxes(self._http)
        self.threads = _AsyncThreads(self._http)
        self.messages = _AsyncMessages(self._http)
        self.attachments = _AsyncAttachments(self._http)
        self.search = _AsyncSearch(self._http)
        self.delivery = _AsyncDelivery(self._http)
        self.phone_numbers = _AsyncPhoneNumbers(self._http)
        self.sms = _AsyncSMS(self._http)
        self.credits = _AsyncCredits(self._http)

    async def close(self) -> None:
        """Close the underlying HTTP connection."""
        await self._http.close()

    async def __aenter__(self) -> AsyncCommuneClient:
        return self

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