"""Commune Python SDK client — mirrors the Node.js SDK API surface."""

from __future__ import annotations

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

from commune._http import HttpClient
from commune.types import (
    Attachment,
    AttachmentUpload,
    AttachmentUrl,
    Domain,
    Inbox,
    Message,
    Thread,
    ThreadList,
)


class _Domains:
    """Domain management.

    Example::

        # List all domains
        domains = client.domains.list()

        # Create a domain
        domain = client.domains.create(name="example.com")

        # Verify DNS
        result = client.domains.verify(domain_id)

        # Get DNS records to configure
        records = client.domains.records(domain_id)
    """

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

    def list(self) -> list[Domain]:
        """List all domains in your organization."""
        data = 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 [])]

    def create(self, name: str, region: str | None = None) -> Domain:
        """Create a new custom domain.

        Args:
            name: The domain name (e.g. "example.com").
            region: Optional region (e.g. "us-east-1").
        """
        payload: dict[str, Any] = {"name": name}
        if region:
            payload["region"] = region
        data = self._http.post("/v1/domains", json=payload)
        return Domain.model_validate(data)

    def get(self, domain_id: str) -> Domain:
        """Get details for a single domain.

        Args:
            domain_id: The domain ID.
        """
        data = self._http.get(f"/v1/domains/{quote(domain_id)}")
        return Domain.model_validate(data)

    def verify(self, domain_id: str) -> dict[str, Any]:
        """Trigger DNS verification for a domain.

        Args:
            domain_id: The domain ID to verify.
        """
        return self._http.post(f"/v1/domains/{quote(domain_id)}/verify")

    def records(self, domain_id: str) -> list[Any]:
        """Get DNS records required for domain verification.

        Args:
            domain_id: The domain ID.

        Returns:
            List of DNS record objects to configure at your registrar.
        """
        data = self._http.get(f"/v1/domains/{quote(domain_id)}/records")
        return data if isinstance(data, list) else []


class _Inboxes:
    """Inbox management.

    Create inboxes with or without specifying a domain. When no domain is
    provided, Commune automatically assigns your inbox to an available domain.

    Example::

        # Simplest — domain is auto-resolved
        inbox = client.inboxes.create(local_part="support")

        # Explicit domain
        inbox = client.inboxes.create(local_part="support", domain_id="d_abc")

        # List all inboxes across all domains
        inboxes = client.inboxes.list()
    """

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

    def list(self, domain_id: Optional[str] = None) -> list[Inbox]:
        """List inboxes.

        Args:
            domain_id: Optional — list inboxes for a specific domain.
                       If omitted, lists all inboxes across all domains.
        """
        if domain_id:
            data = self._http.get(f"/v1/domains/{quote(domain_id)}/inboxes")
        else:
            data = 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 [])]

    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.

        The email address will be ``{local_part}@{domain}``. If no
        ``domain_id`` is provided, Commune auto-assigns your inbox to
        an available domain — no DNS setup required.

        Args:
            local_part: The part before the @ (e.g. ``"support"`` → support@domain.com).
            domain_id: Optional domain to create under. Auto-resolved if omitted.
            name: Optional display name for the inbox.
            webhook: Optional webhook config: ``{"endpoint": "https://...", "events": ["inbound"]}``.
        """
        payload: dict[str, Any] = {"local_part": local_part}
        if name:
            payload["name"] = name
        if webhook:
            payload["webhook"] = webhook

        if domain_id:
            payload["domain_id"] = domain_id

        # Use top-level POST /v1/inboxes (auto-resolves domain)
        data = self._http.post("/v1/inboxes", json=payload)
        return Inbox.model_validate(data)

    def get(self, domain_id: str, inbox_id: str) -> Inbox:
        """Get a single inbox.

        Args:
            domain_id: The domain ID.
            inbox_id: The inbox ID.
        """
        data = self._http.get(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}"
        )
        return Inbox.model_validate(data)

    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.

        Args:
            domain_id: The domain ID.
            inbox_id: The inbox ID.
            local_part: New local part (optional).
            webhook: New webhook config (optional).
            status: New status (optional).
        """
        payload: dict[str, Any] = {}
        if local_part is not None:
            payload["local_part"] = local_part
        if webhook is not None:
            payload["webhook"] = webhook
        if status is not None:
            payload["status"] = status
        data = self._http.put(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}", json=payload
        )
        return Inbox.model_validate(data)

    def remove(self, domain_id: str, inbox_id: str) -> bool:
        """Delete an inbox.

        Args:
            domain_id: The domain ID.
            inbox_id: The inbox ID.

        Returns:
            True if the inbox was deleted.
        """
        data = self._http.delete(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}"
        )
        return bool(data.get("ok")) if isinstance(data, dict) else True

    def set_webhook(
        self,
        domain_id: str,
        inbox_id: str,
        *,
        endpoint: str,
        events: Optional[list[str]] = None,
    ) -> Inbox:
        """Set a webhook on an inbox. Receives events when emails arrive.

        Args:
            domain_id: The domain ID.
            inbox_id: The inbox ID.
            endpoint: The webhook URL to call.
            events: Optional list of event types to subscribe to.
        """
        payload: dict[str, Any] = {"endpoint": endpoint}
        if events:
            payload["events"] = events
        data = self._http.put(
            f"/v1/domains/{quote(domain_id)}/inboxes/{quote(inbox_id)}",
            json={"webhook": payload},
        )
        return Inbox.model_validate(data)


class _Threads:
    """Thread (conversation) management.

    A thread is a group of related email messages sharing a conversation.

    Example::

        # List threads for an inbox (paginated)
        result = client.threads.list(inbox_id="...", limit=20)

        # Get next page
        next_page = client.threads.list(inbox_id="...", cursor=result.next_cursor)

        # Get all messages in a thread
        messages = client.threads.messages(thread_id)
    """

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

    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.

        Args:
            inbox_id: Filter by inbox (recommended).
            domain_id: Filter by domain.
            limit: Max results per page (1–100, default 20).
            cursor: Pagination cursor from a previous response.
            order: Sort order — ``"desc"`` (newest first) or ``"asc"``.

        Returns:
            A ``ThreadList`` with ``data``, ``next_cursor``, and ``has_more``.
        """
        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 = self._http.get("/v1/threads", params=params)

        # The raw response from _unwrap already extracted "data" from the outer wrapper,
        # but for threads the response shape is { data: [...], next_cursor, has_more }
        # We need to handle both cases
        if isinstance(raw, list):
            return ThreadList(data=[Thread.model_validate(t) for t in raw])

        # When the _http.get already unwrapped, we get the full body back
        # because threads endpoint returns {data, next_cursor, has_more}
        # and _unwrap extracts "data" key → raw is the list.
        # If we got a dict, it means the outer wrapper was different.
        if isinstance(raw, dict):
            threads_data = raw.get("data", raw) if "data" in raw else raw
            if isinstance(threads_data, list):
                return ThreadList(
                    data=[Thread.model_validate(t) for t in threads_data],
                    next_cursor=raw.get("next_cursor"),
                    has_more=raw.get("has_more", False),
                )

        return ThreadList(data=[Thread.model_validate(t) for t in (raw or [])])

    def messages(
        self,
        thread_id: str,
        *,
        limit: int = 50,
        order: str = "asc",
    ) -> list[Message]:
        """Get all messages in a thread.

        Args:
            thread_id: The thread (conversation) ID.
            limit: Max messages (1–1000, default 50).
            order: ``"asc"`` (oldest first, default) or ``"desc"``.

        Returns:
            List of messages in the thread.
        """
        data = 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 [])]


class _Messages:
    """Email sending and listing.

    Example::

        # Send an email
        result = client.messages.send(
            to="user@example.com",
            subject="Hello",
            html="<p>Hi there!</p>",
        )

        # List messages for an inbox
        messages = client.messages.list(inbox_id="...")
    """

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

    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,
    ) -> dict[str, Any]:
        """Send an email.

        Args:
            to: Recipient email address(es).
            subject: Email subject line.
            html: HTML body content.
            text: Plain text body content (fallback).
            from_address: Sender address (optional, uses default).
            cc: CC recipients.
            bcc: BCC recipients.
            reply_to: Reply-to address.
            thread_id: Reply within an existing thread.
            domain_id: Send from a specific domain.
            inbox_id: Send from a specific inbox.
            attachments: List of attachment IDs from ``client.attachments.upload()``.
            headers: Custom email headers.
        """
        payload: dict[str, Any] = {"to": to, "subject": subject}
        if html is not None:
            payload["html"] = html
        if text is not None:
            payload["text"] = text
        if from_address:
            payload["from"] = from_address
        if cc:
            payload["cc"] = cc
        if bcc:
            payload["bcc"] = bcc
        if reply_to:
            payload["reply_to"] = reply_to
        if thread_id:
            payload["thread_id"] = thread_id
        if domain_id:
            payload["domainId"] = domain_id
        if inbox_id:
            payload["inboxId"] = inbox_id
        if attachments:
            payload["attachments"] = attachments
        if headers:
            payload["headers"] = headers

        return self._http.post("/v1/messages/send", json=payload)

    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.

        Provide at least one of ``inbox_id``, ``domain_id``, or ``sender``.

        Args:
            inbox_id: Filter by inbox.
            domain_id: Filter by domain.
            sender: Filter by sender email.
            limit: Max results (1–1000, default 50).
            order: ``"asc"`` or ``"desc"`` (default).
            before: ISO date — only messages before this time.
            after: ISO date — only messages after this time.
        """
        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 = 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 _Attachments:
    """Attachment upload and retrieval.

    Example::

        import base64

        # Upload a file
        with open("invoice.pdf", "rb") as f:
            content = base64.b64encode(f.read()).decode()

        upload = client.attachments.upload(
            content=content,
            filename="invoice.pdf",
            mime_type="application/pdf",
        )

        # Use in an email
        client.messages.send(
            to="user@example.com",
            subject="Invoice",
            html="<p>See attached.</p>",
            attachments=[upload.attachment_id],
        )

        # Get download URL
        url_info = client.attachments.url(upload.attachment_id)
    """

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

    def upload(
        self,
        content: str,
        filename: str,
        mime_type: str,
    ) -> AttachmentUpload:
        """Upload an attachment for later use in emails.

        Args:
            content: Base64-encoded file content.
            filename: Original filename (e.g. ``"invoice.pdf"``).
            mime_type: MIME type (e.g. ``"application/pdf"``).

        Returns:
            An ``AttachmentUpload`` with the ``attachment_id`` to use when sending.
        """
        data = self._http.post(
            "/v1/attachments/upload",
            json={"content": content, "filename": filename, "mime_type": mime_type},
        )
        return AttachmentUpload.model_validate(data)

    def get(self, attachment_id: str) -> Attachment:
        """Get attachment metadata.

        Args:
            attachment_id: The attachment ID.
        """
        data = self._http.get(f"/v1/attachments/{quote(attachment_id)}")
        return Attachment.model_validate(data)

    def url(self, attachment_id: str, *, expires_in: int = 3600) -> AttachmentUrl:
        """Get a temporary download URL for an attachment.

        Args:
            attachment_id: The attachment ID.
            expires_in: URL expiration in seconds (default 3600).

        Returns:
            An ``AttachmentUrl`` with the download ``url``.
        """
        data = self._http.get(
            f"/v1/attachments/{quote(attachment_id)}/url",
            params={"expires_in": expires_in},
        )
        return AttachmentUrl.model_validate(data)


class CommuneClient:
    """Commune SDK client.

    Initialize with your API key to access all Commune features::

        from commune import CommuneClient

        client = CommuneClient(api_key="comm_...")

        # Manage domains
        domains = client.domains.list()

        # Manage inboxes
        inbox = client.inboxes.create(domain_id, local_part="support")

        # Browse threads
        threads = client.threads.list(inbox_id=inbox.id)

        # Read messages
        messages = client.threads.messages(threads.data[0].thread_id)

        # Send emails
        client.messages.send(to="user@example.com", subject="Hi", html="<p>Hello!</p>")

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

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str | None = None,
        timeout: float = 30.0,
    ):
        self._http = HttpClient(api_key=api_key, base_url=base_url, timeout=timeout)
        self.domains = _Domains(self._http)
        self.inboxes = _Inboxes(self._http)
        self.threads = _Threads(self._http)
        self.messages = _Messages(self._http)
        self.attachments = _Attachments(self._http)

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

    def __enter__(self) -> CommuneClient:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()
