"""Mail client — admin operations and Mailbox for agent operations."""

from __future__ import annotations

import os
import time as _time
from typing import Literal

import httpx

from primitif._models import PaginatedList
from primitif._transport import (
    DEFAULT_MAX_RETRIES,
    RETRY_STATUS_CODES,
    _retry_delay,
    _transport_retry_delay,
    configure_logging,
    raise_for_status,
    request_with_retries,
)
from primitif.mail.exceptions import (
    MailAuthError,
    MailConflictError,
    MailNetworkError,
    MailError,
    MailNotFoundError,
    MailRateLimitError,
    MailServerError,
    MailValidationError,
)
from primitif.mail.models import (
    AccountInfo,
    AllowlistEntry,
    AttachmentInfo,
    InboxMessage,
    MailboxInfo,
    MailboxListItem,
    MessageDetail,
    PlanLimits,
    ProtectedRecipient,
    SendResult,
    ThreadDetail,
    ThreadSummary,
    WebhookCreated,
    WebhookInfo,
)

DEFAULT_BASE_URL = "https://api.mail.primitif.ai"

logger = configure_logging("mail")

_EXC_MAP = {
    401: MailAuthError,
    404: MailNotFoundError,
    409: MailConflictError,
    422: MailValidationError,
    429: MailRateLimitError,
}


class Mailbox:
    """A single ephemeral mailbox — send, receive, and manage email.

    Typically created via ``p.mail.create_mailbox()``, but can also
    be instantiated directly with a token for agent-scoped operations.

    Usage::

        with Mailbox() as mb:  # reads PRIMITIF_MAIL_TOKEN from env
            mb.send(to="user@example.com", subject="Hi", body_text="Hello")
            for msg in mb.inbox():
                detail = mb.read(msg.id)
            mb.delete()
    """

    def __init__(
        self,
        token: str | None = None,
        *,
        base_url: str | None = None,
        max_retries: int = DEFAULT_MAX_RETRIES,
        timeout: float = 30.0,
        _client: httpx.Client | None = None,
    ) -> None:
        self._destroyed = False
        self._token: str | None = None

        if _client is not None:
            # Internal: created by MailAdmin.create_mailbox()
            self._client = _client
            self._max_retries = max_retries
            self._info_cache: MailboxInfo | None = None
            self._owns_client = False
            return

        from primitif._version import __version__

        if token is None:
            token = os.environ.get("PRIMITIF_MAIL_TOKEN")
        if token is not None:
            token = token.strip()
        if not token:
            raise MailError(
                message="No token provided. Pass token= or set PRIMITIF_MAIL_TOKEN env var."
            )

        self._token = token

        if base_url is None:
            base_url = os.environ.get("PRIMITIF_MAIL_URL", DEFAULT_BASE_URL)

        self._max_retries = max_retries
        self._info_cache = None
        self._owns_client = True
        self._client = httpx.Client(
            base_url=base_url.rstrip("/"),
            headers={
                "Authorization": f"Bearer {token}",
                "User-Agent": f"primitif/{__version__}",
            },
            timeout=timeout,
        )

    def _request(self, method: str, path: str, **kwargs) -> dict:
        if self._destroyed:
            raise MailError(message="Mailbox has been destroyed.")
        return request_with_retries(
            self._client,
            method,
            path,
            self._max_retries,
            exc_map=_EXC_MAP,
            server_exc=MailServerError,
            base_exc=MailError,
            connection_exc=MailNetworkError,
            logger=logger,
            **kwargs,
        )

    # ── Info ──

    @property
    def address(self) -> str:
        """The mailbox email address (fetched and cached)."""
        if self._info_cache is None:
            self.info()
        return self._info_cache.address

    @property
    def token(self) -> str | None:
        """The mailbox token, if available.

        Present when created via ``p.mail.create_mailbox()`` or when
        ``token=`` was passed to the constructor. Useful for persisting
        tokens so a crashed agent can reconnect with ``Mailbox(token=...)``.
        """
        return self._token

    def info(self) -> MailboxInfo:
        """Get mailbox details."""
        data = self._request("GET", "/v1/me")
        result = MailboxInfo(**data)
        self._info_cache = result
        return result

    # ── Inbox & messages ──

    def inbox(
        self,
        *,
        unread_only: bool = False,
        limit: int = 20,
        cursor: str | None = None,
    ) -> PaginatedList[InboxMessage]:
        """List inbox messages."""
        params: dict = {"limit": limit}
        if unread_only:
            params["unread_only"] = "true"
        if cursor is not None:
            params["cursor"] = cursor
        data = self._request("GET", "/v1/me/inbox", params=params)
        items = [InboxMessage(**m) for m in data["items"]]
        result = PaginatedList(
            items=items, cursor=data.get("cursor"), has_more=data.get("has_more", False)
        )
        result._fetch_fn = self.inbox
        result._fetch_kwargs = {"unread_only": unread_only, "limit": limit}
        return result

    def read(self, message_id: str) -> MessageDetail:
        """Read a full message."""
        data = self._request("GET", f"/v1/me/messages/{message_id}")
        return MessageDetail(**data)

    def threads(
        self,
        *,
        limit: int = 20,
        cursor: str | None = None,
    ) -> PaginatedList[ThreadSummary]:
        """List email threads."""
        params: dict = {"limit": limit}
        if cursor is not None:
            params["cursor"] = cursor
        data = self._request("GET", "/v1/me/threads", params=params)
        items = [ThreadSummary(**t) for t in data["items"]]
        result = PaginatedList(
            items=items, cursor=data.get("cursor"), has_more=data.get("has_more", False)
        )
        result._fetch_fn = self.threads
        result._fetch_kwargs = {"limit": limit}
        return result

    def thread(self, thread_id: str) -> ThreadDetail:
        """Get a full thread with all messages."""
        data = self._request("GET", f"/v1/me/threads/{thread_id}")
        messages = [MessageDetail(**m) for m in data.pop("messages", [])]
        return ThreadDetail(**data, messages=messages)

    # ── Sending ──

    def send(
        self,
        *,
        to: str,
        subject: str,
        body_text: str,
        body_html: str | None = None,
        require_approval: bool = False,
    ) -> SendResult:
        """Send an email from this mailbox."""
        payload: dict = {"to": to, "subject": subject, "body_text": body_text}
        if body_html is not None:
            payload["body_html"] = body_html
        if require_approval:
            payload["require_approval"] = True
        data = self._request("POST", "/v1/me/send", json=payload)
        return SendResult(**data)

    def reply(
        self,
        message_id: str,
        *,
        body_text: str,
        body_html: str | None = None,
        require_approval: bool = False,
    ) -> SendResult:
        """Reply to a message."""
        payload: dict = {"body_text": body_text}
        if body_html is not None:
            payload["body_html"] = body_html
        if require_approval:
            payload["require_approval"] = True
        data = self._request("POST", f"/v1/me/reply/{message_id}", json=payload)
        return SendResult(**data)

    # ── Attachments ──

    def list_attachments(self, message_id: str) -> list[AttachmentInfo]:
        """List attachments on a message."""
        data = self._request("GET", f"/v1/me/messages/{message_id}/attachments")
        return [AttachmentInfo(**a) for a in data["items"]]

    def download_attachment(self, message_id: str, attachment_id: str) -> bytes:
        """Download a raw attachment."""
        if self._destroyed:
            raise MailError(message="Mailbox has been destroyed.")
        path = f"/v1/me/messages/{message_id}/attachments/{attachment_id}"
        last_response: httpx.Response | None = None
        last_exc: httpx.TransportError | None = None
        for attempt in range(self._max_retries + 1):
            try:
                response = self._client.request("GET", path)
            except (httpx.TimeoutException, httpx.ConnectError) as exc:
                last_exc = exc
                if attempt < self._max_retries:
                    _time.sleep(_transport_retry_delay(attempt))
                    continue
                raise MailNetworkError(
                    message=f"{type(exc).__name__}: {exc}",
                ) from exc
            except httpx.TransportError as exc:
                raise MailNetworkError(
                    message=f"{type(exc).__name__}: {exc}",
                ) from exc
            last_exc = None
            last_response = response
            if response.status_code in RETRY_STATUS_CODES and attempt < self._max_retries:
                _time.sleep(_retry_delay(response, attempt))
                continue
            break
        if last_response is None:
            raise MailNetworkError(
                message=f"{type(last_exc).__name__}: {last_exc}",
            ) from last_exc
        raise_for_status(
            last_response,
            exc_map=_EXC_MAP,
            server_exc=MailServerError,
            base_exc=MailError,
        )
        return last_response.content

    # ── Allowlist ──

    def get_allowlist(self) -> list[AllowlistEntry]:
        """List allowed senders."""
        data = self._request("GET", "/v1/me/allowlist")
        return [AllowlistEntry(**e) for e in data["items"]]

    def add_allowlist(self, sender_address: str) -> AllowlistEntry:
        """Add an allowed sender."""
        data = self._request(
            "POST", "/v1/me/allowlist", json={"sender_address": sender_address}
        )
        return AllowlistEntry(**data)

    def remove_allowlist(self, entry_id: str) -> None:
        """Remove an allowed sender."""
        self._request("DELETE", f"/v1/me/allowlist/{entry_id}")

    # ── Auth policy ──

    def get_auth_policy(self) -> str:
        """Get the email authentication policy."""
        data = self._request("GET", "/v1/me/auth-policy")
        return data["auth_policy"]

    def set_auth_policy(self, policy: Literal["none", "warn", "reject"]) -> None:
        """Set the email authentication policy."""
        self._request("PUT", "/v1/me/auth-policy", json={"auth_policy": policy})

    # ── Webhooks ──

    def set_webhook(self, url: str, events: list[str] | None = None) -> WebhookCreated:
        """Register a webhook. Returns WebhookCreated with signing secret (shown once)."""
        payload: dict = {"url": url}
        if events is not None:
            payload["events"] = events
        data = self._request("POST", "/v1/me/webhook", json=payload)
        return WebhookCreated(**data)

    def webhook(self) -> WebhookInfo:
        """Get current webhook config."""
        data = self._request("GET", "/v1/me/webhook")
        return WebhookInfo(**data)

    def delete_webhook(self) -> None:
        """Remove the webhook."""
        self._request("DELETE", "/v1/me/webhook")

    # ── Lifecycle ──

    def delete(self) -> None:
        """Delete this mailbox from the server and close the HTTP client."""
        self._request("DELETE", "/v1/me")
        self._destroyed = True
        if self._owns_client:
            self._client.close()

    def close(self) -> None:
        """Close the HTTP client."""
        if self._owns_client:
            self._client.close()

    def __enter__(self) -> Mailbox:
        return self

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

    def __repr__(self) -> str:
        if self._info_cache:
            return f"Mailbox({self._info_cache.address})"
        return "Mailbox()"


class MailAdmin:
    """Mail admin operations — create/list/delete mailboxes, protected recipients, account info.

    Not instantiated directly. Access via ``Primitif().mail``.
    """

    def __init__(self, client: httpx.Client, max_retries: int, base_url: str) -> None:
        self._client = client
        self._max_retries = max_retries
        self._base_url = base_url

    def _request(self, method: str, path: str, **kwargs) -> dict:
        return request_with_retries(
            self._client,
            method,
            path,
            self._max_retries,
            exc_map=_EXC_MAP,
            server_exc=MailServerError,
            base_exc=MailError,
            connection_exc=MailNetworkError,
            logger=logger,
            **kwargs,
        )

    def create_mailbox(self, name: str | None = None, ttl: int | None = None) -> Mailbox:
        """Create a new mailbox. Returns a Mailbox ready to use.

        Args:
            name: Mailbox name (used in address prefix). Omit for a random name.
            ttl: Optional TTL in seconds (max 30 days).
        """
        payload: dict = {}
        if name is not None:
            payload["name"] = name
        if ttl is not None:
            payload["ttl_seconds"] = ttl
        data = self._request("POST", "/v1/mailboxes", json=payload)

        from primitif._version import __version__

        token = data["token"]
        mb_client = httpx.Client(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {token}",
                "User-Agent": f"primitif/{__version__}",
            },
            timeout=self._client.timeout,
        )
        try:
            mb = Mailbox(_client=mb_client, max_retries=self._max_retries)
            mb._token = token
            mb._info_cache = MailboxInfo(
                id=data["id"],
                address=data["address"],
                name=name or data.get("name", ""),
                expires_at=data.get("expires_at"),
            )
            mb._owns_client = True
        except BaseException:
            mb_client.close()
            raise
        return mb

    def list_mailboxes(
        self,
        *,
        limit: int = 20,
        cursor: str | None = None,
    ) -> PaginatedList[MailboxListItem]:
        """List all mailboxes in the org."""
        params: dict = {"limit": limit}
        if cursor is not None:
            params["cursor"] = cursor
        data = self._request("GET", "/v1/mailboxes", params=params)
        items = [MailboxListItem(**m) for m in data["items"]]
        result = PaginatedList(
            items=items, cursor=data.get("cursor"), has_more=data.get("has_more", False)
        )
        result._fetch_fn = self.list_mailboxes
        result._fetch_kwargs = {"limit": limit}
        return result

    def delete_mailbox(self, mailbox_id: str) -> None:
        """Delete a mailbox by ID."""
        self._request("DELETE", f"/v1/mailboxes/{mailbox_id}")

    def account(self) -> AccountInfo:
        """Get account info (plan, usage, limits)."""
        data = self._request("GET", "/v1/account")
        return AccountInfo(**data)

    def list_protected_recipients(self) -> list[ProtectedRecipient]:
        """List all protected recipient patterns."""
        data = self._request("GET", "/v1/protected-recipients")
        return [ProtectedRecipient(**e) for e in data["items"]]

    def add_protected_recipient(self, pattern: str) -> ProtectedRecipient:
        """Add a protected recipient pattern (email or @domain)."""
        data = self._request("POST", "/v1/protected-recipients", json={"pattern": pattern})
        return ProtectedRecipient(**data)

    def remove_protected_recipient(self, entry_id: str) -> None:
        """Remove a protected recipient pattern."""
        self._request("DELETE", f"/v1/protected-recipients/{entry_id}")
