"""
Commune MCP Server — email infrastructure tools for AI agents.

An MCP server that connects AI agents to Commune's email API.
Manages domains, inboxes, threads, messages, and attachments.

Install:
    pip install commune-mcp

Configure in Claude Desktop / Cursor / Windsurf:
    {
        "mcpServers": {
            "commune": {
                "command": "uvx",
                "args": ["commune-mcp"],
                "env": { "COMMUNE_API_KEY": "comm_..." }
            }
        }
    }
"""

from __future__ import annotations

import contextvars
import json
import os
import sys
from typing import Any, Literal, Optional

import httpx
from mcp.server.fastmcp import FastMCP

# ── Configuration ────────────────────────────────────────────────────────────

MCP_VERSION = "0.2.0"
API_VERSION = "v1"  # Commune API version this MCP is tested against
MIN_API_VERSION = "v1"  # Minimum compatible API version

# Per-request API key — set by HTTP middleware; falls back to env var for stdio mode.
_api_key_ctx: contextvars.ContextVar[str] = contextvars.ContextVar(
    "commune_api_key", default=os.environ.get("COMMUNE_API_KEY", "")
)

BASE_URL = os.environ.get(
    "COMMUNE_BASE_URL", "https://api.commune.email"
).rstrip("/")

mcp = FastMCP(
    "Commune",
    instructions=(
        "Email infrastructure for agents — set up an inbox and send your first email in 30 seconds. "
        "Programmatic inboxes (~1 line), consistent threads, custom domains, attachments, and structured data. "
        "Use submit_feedback to report errors, request missing capabilities, or send observations back to the product team."
    ),
)

# ── HTTP helpers ─────────────────────────────────────────────────────────────

def _headers() -> dict[str, str]:
    return {
        "Authorization": f"Bearer {_api_key_ctx.get()}",
        "Content-Type": "application/json",
    }


def _get(path: str, params: Optional[dict[str, Any]] = None) -> Any:
    """GET request to the Commune v1 API."""
    clean = {k: v for k, v in (params or {}).items() if v is not None}
    resp = httpx.get(
        f"{BASE_URL}{path}", headers=_headers(), params=clean or None, timeout=30
    )
    resp.raise_for_status()
    body = resp.json()
    return body.get("data", body) if isinstance(body, dict) else body


def _post(path: str, payload: Optional[dict[str, Any]] = None) -> Any:
    """POST request to the Commune v1 API."""
    resp = httpx.post(
        f"{BASE_URL}{path}", headers=_headers(), json=payload, timeout=30
    )
    resp.raise_for_status()
    body = resp.json()
    return body.get("data", body) if isinstance(body, dict) else body


def _put(path: str, payload: Optional[dict[str, Any]] = None) -> Any:
    """PUT request to the Commune v1 API."""
    resp = httpx.put(
        f"{BASE_URL}{path}", headers=_headers(), json=payload, timeout=30
    )
    resp.raise_for_status()
    body = resp.json()
    return body.get("data", body) if isinstance(body, dict) else body


def _delete(path: str) -> Any:
    """DELETE request to the Commune v1 API."""
    resp = httpx.delete(f"{BASE_URL}{path}", headers=_headers(), timeout=30)
    resp.raise_for_status()
    body = resp.json()
    return body.get("data", body) if isinstance(body, dict) else body


def _delete_with_body(path: str, payload: dict[str, Any]) -> Any:
    """DELETE request with JSON body to the Commune v1 API."""
    resp = httpx.request("DELETE", f"{BASE_URL}{path}", headers=_headers(), json=payload, timeout=30)
    resp.raise_for_status()
    body = resp.json()
    return body.get("data", body) if isinstance(body, dict) else body


def _fmt(data: Any) -> str:
    """Format data as indented JSON for readable tool output."""
    return json.dumps(data, indent=2, default=str)


# ═════════════════════════════════════════════════════════════════════════════
# DOMAIN TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def list_domains() -> str:
    """List all email domains in your Commune account.

    Returns each domain's ID, name, and verification status.
    Use the domain ID with other tools like list_inboxes or create_inbox.
    """
    return _fmt(_get("/v1/domains"))


@mcp.tool()
def create_domain(name: str, region: Optional[str] = None) -> str:
    """Create a new custom email domain.

    After creating a domain, you need to:
    1. Call get_domain_records to see the required DNS records
    2. Add those records at your domain registrar
    3. Call verify_domain to check verification status

    Args:
        name: Domain name, e.g. "example.com"
        region: AWS region (optional), e.g. "us-east-1" or "eu-west-1"
    """
    payload: dict[str, Any] = {"name": name}
    if region:
        payload["region"] = region
    return _fmt(_post("/v1/domains", payload))


@mcp.tool()
def verify_domain(domain_id: str) -> str:
    """Trigger DNS verification for a domain.

    Call this after adding the required DNS records at your registrar.
    Use get_domain_records first to see which records are needed.

    Args:
        domain_id: The domain ID (from list_domains)
    """
    return _fmt(_post(f"/v1/domains/{domain_id}/verify"))


@mcp.tool()
def get_domain_records(domain_id: str) -> str:
    """Get the DNS records required to verify a domain.

    Returns MX, TXT, and CNAME records that must be added
    at your domain registrar before calling verify_domain.

    Args:
        domain_id: The domain ID (from list_domains)
    """
    return _fmt(_get(f"/v1/domains/{domain_id}/records"))


# ═════════════════════════════════════════════════════════════════════════════
# INBOX TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def list_inboxes(domain_id: Optional[str] = None) -> str:
    """List inboxes.

    Without domain_id, lists all inboxes across all domains.
    With domain_id, lists inboxes for that specific domain.

    Each inbox has a local_part (the part before @) that forms
    the email address: {local_part}@{domain_name}

    Args:
        domain_id: Filter by domain (optional, lists all if omitted)
    """
    if domain_id:
        return _fmt(_get(f"/v1/domains/{domain_id}/inboxes"))
    return _fmt(_get("/v1/inboxes"))


@mcp.tool()
def create_inbox(
    local_part: str,
    domain_id: Optional[str] = None,
    name: Optional[str] = None,
    display_name: Optional[str] = None,
    webhook_endpoint: Optional[str] = None,
) -> str:
    """Create a new inbox for receiving emails.

    The inbox 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: Part before @ (e.g. "support", "billing", "hello")
        domain_id: Domain to create under (optional, auto-resolved if omitted)
        name: Agent name for the inbox (optional, also used as display_name fallback)
        display_name: Sender display name shown in email clients (e.g. "Support Agent", "Acme Sales"). If set, outbound emails show as '"Display Name" <email>' in Gmail/Outlook.
        webhook_endpoint: URL to receive notifications on new emails (optional)
    """
    payload: dict[str, Any] = {"local_part": local_part}
    if domain_id:
        payload["domain_id"] = domain_id
    if name:
        payload["name"] = name
    if display_name:
        payload["display_name"] = display_name
    if webhook_endpoint:
        payload["webhook"] = {"endpoint": webhook_endpoint}
    return _fmt(_post("/v1/inboxes", payload))


@mcp.tool()
def delete_inbox(domain_id: str, inbox_id: str) -> str:
    """Delete an inbox.

    Args:
        domain_id: The domain ID
        inbox_id: The inbox ID to delete
    """
    return _fmt(_delete(f"/v1/domains/{domain_id}/inboxes/{inbox_id}"))


@mcp.tool()
def set_extraction_schema(
    domain_id: str,
    inbox_id: str,
    name: str,
    schema: str,
    description: Optional[str] = None,
    enabled: bool = True,
) -> str:
    """Set structured extraction schema for an inbox.

    Args:
        domain_id: Domain ID for the inbox.
        inbox_id: Inbox ID.
        name: Schema name.
        schema: JSON string of a valid JSON Schema object.
        description: Optional schema description.
        enabled: Enable extraction immediately (default: true).
    """
    try:
        parsed_schema = json.loads(schema)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid schema JSON: {exc}") from exc

    payload: dict[str, Any] = {
        "name": name,
        "schema": parsed_schema,
        "enabled": enabled,
    }
    if description:
        payload["description"] = description

    return _fmt(
        _put(
            f"/v1/domains/{domain_id}/inboxes/{inbox_id}/extraction-schema",
            payload,
        )
    )


@mcp.tool()
def remove_extraction_schema(domain_id: str, inbox_id: str) -> str:
    """Remove structured extraction schema from an inbox."""
    return _fmt(_delete(f"/v1/domains/{domain_id}/inboxes/{inbox_id}/extraction-schema"))


# ═════════════════════════════════════════════════════════════════════════════
# THREAD TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def list_threads(
    inbox_id: Optional[str] = None,
    domain_id: Optional[str] = None,
    limit: int = 20,
    cursor: Optional[str] = None,
    order: str = "desc",
) -> str:
    """List email threads (conversations) with pagination.

    Returns thread summaries: subject, message count, last activity, snippet.
    Use next_cursor from the response to fetch the next page.

    Provide at least one of inbox_id or domain_id.

    Args:
        inbox_id: Filter threads by inbox (recommended)
        domain_id: Filter threads by domain
        limit: Results per page, 1-100 (default: 20)
        cursor: Pagination cursor from a previous response's next_cursor
        order: "desc" for newest first (default), "asc" for oldest first
    """
    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

    # Thread endpoint returns {data, next_cursor, has_more} — return full body
    resp = httpx.get(
        f"{BASE_URL}/v1/threads",
        headers=_headers(),
        params=params,
        timeout=30,
    )
    resp.raise_for_status()
    return _fmt(resp.json())


@mcp.tool()
def get_thread_messages(
    thread_id: str,
    limit: int = 50,
    order: str = "asc",
) -> str:
    """Get all messages in an email thread.

    Returns the full conversation with sender, content, timestamps.

    Args:
        thread_id: The thread ID (from list_threads)
        limit: Max messages, 1-1000 (default: 50)
        order: "asc" for chronological (default), "desc" for newest first
    """
    return _fmt(
        _get(
            f"/v1/threads/{thread_id}/messages",
            {"limit": limit, "order": order},
        )
    )


# ═════════════════════════════════════════════════════════════════════════════
# MESSAGE TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def send_email(
    to: str,
    subject: str,
    html: Optional[str] = None,
    text: Optional[str] = None,
    from_address: Optional[str] = None,
    reply_to: Optional[str] = None,
    thread_id: Optional[str] = None,
    inbox_id: Optional[str] = None,
    domain_id: Optional[str] = None,
    attachments: Optional[str] = None,
) -> str:
    """Send an email message.

    Provide html or text (or both) for the body.
    To reply in an existing thread, pass thread_id.
    To attach files, first call upload_attachment, then pass
    the attachment IDs as a comma-separated string.

    You only need inbox_id to send — the domain is inferred automatically.

    Args:
        to: Recipient email address (for multiple, comma-separate)
        subject: Email subject line
        html: HTML body content
        text: Plain text body (fallback)
        from_address: Sender address (optional, uses inbox default)
        reply_to: Reply-to address (optional)
        thread_id: Reply within an existing thread (optional)
        inbox_id: Send from a specific inbox (recommended — domain is auto-resolved)
        domain_id: Send from a specific domain (optional, inferred from inbox_id)
        attachments: Comma-separated attachment IDs from upload_attachment (optional)
    """
    # Parse comma-separated to into list
    to_list = [addr.strip() for addr in to.split(",") if addr.strip()]

    payload: dict[str, Any] = {
        "to": to_list if len(to_list) > 1 else to_list[0],
        "subject": subject,
    }
    if html:
        payload["html"] = html
    if text:
        payload["text"] = text
    if from_address:
        payload["from"] = from_address
    if reply_to:
        payload["reply_to"] = reply_to
    if thread_id:
        payload["thread_id"] = thread_id
    if inbox_id:
        payload["inboxId"] = inbox_id
    if domain_id:
        payload["domainId"] = domain_id
    if attachments:
        payload["attachments"] = [
            a.strip() for a in attachments.split(",") if a.strip()
        ]

    return _fmt(_post("/v1/messages/send", payload))


# ═════════════════════════════════════════════════════════════════════════════
# ATTACHMENT TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def upload_attachment(
    content: str,
    filename: str,
    mime_type: str,
) -> str:
    """Upload a file for use when sending emails.

    Returns an attachment_id to pass to send_email's attachments parameter.

    Args:
        content: Base64-encoded file content
        filename: Original filename, e.g. "report.pdf"
        mime_type: MIME type, e.g. "application/pdf" or "image/png"
    """
    return _fmt(
        _post(
            "/v1/attachments/upload",
            {"content": content, "filename": filename, "mime_type": mime_type},
        )
    )


@mcp.tool()
def get_attachment_url(attachment_id: str, expires_in: int = 3600) -> str:
    """Get a temporary download URL for an attachment.

    Args:
        attachment_id: The attachment ID
        expires_in: URL lifetime in seconds (default: 3600 = 1 hour)
    """
    return _fmt(
        _get(
            f"/v1/attachments/{attachment_id}/url",
            {"expires_in": expires_in},
        )
    )


# ═════════════════════════════════════════════════════════════════════════════
# SEARCH TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def search_threads(
    query: str,
    inbox_id: Optional[str] = None,
    domain_id: Optional[str] = None,
    limit: int = 20,
) -> str:
    """Search across email threads by subject or content.

    Returns matching thread summaries with subject, snippet, and message count.
    Provide at least one of inbox_id or domain_id.

    Args:
        query: Search query (searches subject and message content)
        inbox_id: Filter by inbox (recommended)
        domain_id: Filter by domain
        limit: Max results, 1-100 (default: 20)
    """
    params: dict[str, Any] = {"q": query, "limit": limit}
    if inbox_id:
        params["inbox_id"] = inbox_id
    if domain_id:
        params["domain_id"] = domain_id
    return _fmt(_get("/v1/search/threads", params))


# ═════════════════════════════════════════════════════════════════════════════
# TRIAGE TOOLS (tags, status, assignment)
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def get_thread_metadata(thread_id: str) -> str:
    """Get triage metadata for a thread: tags, status, and assignment.

    Args:
        thread_id: The thread ID
    """
    return _fmt(_get(f"/v1/threads/{thread_id}/metadata"))


@mcp.tool()
def set_thread_status(
    thread_id: str,
    status: str,
) -> str:
    """Set the status of a thread for triage.

    Valid statuses: "open", "needs_reply", "waiting", "closed"

    Args:
        thread_id: The thread ID
        status: New status — one of: open, needs_reply, waiting, closed
    """
    return _fmt(_put(f"/v1/threads/{thread_id}/status", {"status": status}))


@mcp.tool()
def tag_thread(
    thread_id: str,
    tags: str,
) -> str:
    """Add tags/labels to a thread. Tags are additive — existing tags are preserved.

    Use tags for categorization: "vip", "bug-report", "sales-lead", "urgent", etc.

    Args:
        thread_id: The thread ID
        tags: Comma-separated tags to add (e.g. "urgent,vip,sales-lead")
    """
    tag_list = [t.strip() for t in tags.split(",") if t.strip()]
    return _fmt(_post(f"/v1/threads/{thread_id}/tags", {"tags": tag_list}))


@mcp.tool()
def untag_thread(
    thread_id: str,
    tags: str,
) -> str:
    """Remove tags/labels from a thread.

    Args:
        thread_id: The thread ID
        tags: Comma-separated tags to remove (e.g. "urgent,vip")
    """
    tag_list = [t.strip() for t in tags.split(",") if t.strip()]
    return _fmt(_delete_with_body(f"/v1/threads/{thread_id}/tags", {"tags": tag_list}))


@mcp.tool()
def assign_thread(
    thread_id: str,
    assigned_to: Optional[str] = None,
) -> str:
    """Assign a thread to an agent or user. Pass null/empty to unassign.

    Args:
        thread_id: The thread ID
        assigned_to: Agent/user identifier to assign to (empty or omit to unassign)
    """
    return _fmt(
        _put(
            f"/v1/threads/{thread_id}/assign",
            {"assigned_to": assigned_to if assigned_to else None},
        )
    )


# ═════════════════════════════════════════════════════════════════════════════
# DELIVERABILITY TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def get_deliverability_stats(
    inbox_id: Optional[str] = None,
    domain_id: Optional[str] = None,
    period: str = "7d",
) -> str:
    """Get email deliverability metrics: sent, delivered, bounced, complained, failed.

    Provides bounce rate, complaint rate, and delivery rate percentages.
    Use this to monitor sender reputation and identify deliverability issues.

    Args:
        inbox_id: Filter metrics by inbox (recommended)
        domain_id: Filter metrics by domain
        period: Time period — "24h", "7d", "30d" (default: "7d")
    """
    params: dict[str, Any] = {"period": period}
    if inbox_id:
        params["inbox_id"] = inbox_id
    if domain_id:
        params["domain_id"] = domain_id
    return _fmt(_get("/v1/delivery/metrics", params))


@mcp.tool()
def get_suppressions(
    inbox_id: Optional[str] = None,
    domain_id: Optional[str] = None,
    limit: int = 50,
) -> str:
    """List suppressed email addresses (bounces, complaints, unsubscribes).

    Suppressed addresses are automatically skipped when sending.
    Use this to audit why certain recipients aren't receiving emails.

    Args:
        inbox_id: Filter by inbox (optional)
        domain_id: Filter by domain (optional)
        limit: Max results (default: 50)
    """
    params: dict[str, Any] = {"limit": limit}
    if inbox_id:
        params["inbox_id"] = inbox_id
    if domain_id:
        params["domain_id"] = domain_id
    return _fmt(_get("/v1/delivery/suppressions", params))


@mcp.tool()
def get_delivery_events(
    message_id: Optional[str] = None,
    inbox_id: Optional[str] = None,
    domain_id: Optional[str] = None,
    event_type: Optional[str] = None,
    limit: int = 50,
) -> str:
    """Get delivery event log: sent, delivered, bounced, complained, failed.

    Track the lifecycle of individual emails or audit delivery across an inbox.

    Args:
        message_id: Filter events for a specific message
        inbox_id: Filter events by inbox
        domain_id: Filter events by domain
        event_type: Filter by type: "sent", "delivered", "bounced", "complained", "failed"
        limit: Max results (default: 50)
    """
    params: dict[str, Any] = {"limit": limit}
    if message_id:
        params["message_id"] = message_id
    if inbox_id:
        params["inbox_id"] = inbox_id
    if domain_id:
        params["domain_id"] = domain_id
    if event_type:
        params["event_type"] = event_type
    return _fmt(_get("/v1/delivery/events", params))


# ═════════════════════════════════════════════════════════════════════════════
# PHONE NUMBER TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def list_phone_numbers() -> str:
    """List all provisioned phone numbers in your Commune account.

    Returns each number's ID, number, type, capabilities, and status.
    Use the phone_number_id with SMS tools or update_phone_number.
    """
    return _fmt(_get("/v1/phone-numbers"))


@mcp.tool()
def get_phone_number(phone_number_id: str) -> str:
    """Get details for a single provisioned phone number.

    Args:
        phone_number_id: The phone number ID (from list_phone_numbers)
    """
    return _fmt(_get(f"/v1/phone-numbers/{phone_number_id}"))


@mcp.tool()
def update_phone_number(
    phone_number_id: str,
    friendly_name: Optional[str] = None,
    auto_reply: Optional[str] = None,
) -> str:
    """Update a phone number's friendly name or auto-reply message.

    Args:
        phone_number_id: The phone number ID (from list_phone_numbers)
        friendly_name: Human-readable label for this number (optional)
        auto_reply: Automatic reply text sent to all inbound SMS (optional, empty string to disable)
    """
    payload: dict[str, Any] = {}
    if friendly_name is not None:
        payload["friendly_name"] = friendly_name
    if auto_reply is not None:
        payload["auto_reply"] = auto_reply
    return _fmt(_put(f"/v1/phone-numbers/{phone_number_id}", payload))


@mcp.tool()
def list_available_phone_numbers(type: str = "TollFree", country: str = "US", limit: int = 10) -> str:
    """List available phone numbers to purchase.

    Browse numbers before buying with provision_phone_number.

    Args:
        type: Number type — "TollFree" (default) or "Local"
        country: Two-letter country code (default: "US")
        limit: Max results (default: 10)
    """
    return _fmt(_get("/v1/phone-numbers/available", {"type": type, "country": country, "limit": limit}))


@mcp.tool()
def provision_phone_number(
    phone_number: Optional[str] = None,
    type: str = "tollfree",
    friendly_name: Optional[str] = None,
) -> str:
    """Purchase/provision a phone number for SMS.

    Deducts credits from your balance. Use list_available_phone_numbers first to browse options.

    Args:
        phone_number: Specific E.164 number to buy, e.g. "+18005551234" (optional, auto-selected if omitted)
        type: Number type — "tollfree" (default) or "local"
        friendly_name: Human-readable label for this number (optional)
    """
    payload: dict[str, Any] = {"type": type}
    if phone_number:
        payload["phone_number"] = phone_number
    if friendly_name:
        payload["friendly_name"] = friendly_name
    return _fmt(_post("/v1/phone-numbers", payload))


@mcp.tool()
def release_phone_number(phone_number_id: str) -> str:
    """Release a provisioned phone number.

    The number will be returned to the pool. No credit refund. Message history is retained.

    Args:
        phone_number_id: The phone number ID to release (from list_phone_numbers)
    """
    return _fmt(_delete(f"/v1/phone-numbers/{phone_number_id}"))


# ═════════════════════════════════════════════════════════════════════════════
# SMS TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def send_sms(
    to: str,
    body: str,
    phone_number_id: Optional[str] = None,
) -> str:
    """Send an SMS message.

    Returns message_id, message_sid, status, and credits_charged.

    Args:
        to: Recipient phone number in E.164 format, e.g. "+15551234567"
        body: SMS message text
        phone_number_id: Send from a specific phone number (optional, auto-assigned if omitted)
    """
    payload: dict[str, Any] = {"to": to, "body": body}
    if phone_number_id:
        payload["phone_number_id"] = phone_number_id
    return _fmt(_post("/v1/sms/send", payload))


@mcp.tool()
def list_sms_conversations(
    phone_number_id: Optional[str] = None,
    limit: int = 20,
) -> str:
    """List SMS conversation threads.

    Returns a summary of each thread: remote number, last message snippet, and message count.

    Args:
        phone_number_id: Filter conversations by phone number (optional, lists all if omitted)
        limit: Max results, 1-100 (default: 20)
    """
    params: dict[str, Any] = {"limit": limit}
    if phone_number_id:
        params["phone_number_id"] = phone_number_id
    return _fmt(_get("/v1/sms/conversations", params))


@mcp.tool()
def get_sms_thread(
    remote_number: str,
    phone_number_id: str,
) -> str:
    """Get all messages in an SMS thread with a specific number.

    Returns chronological messages with direction, body, and timestamps.

    Args:
        remote_number: The external phone number in E.164 format, e.g. "+15551234567"
        phone_number_id: Your Commune phone number ID for this conversation
    """
    from urllib.parse import quote
    return _fmt(
        _get(
            f"/v1/sms/conversations/{quote(remote_number, safe='')}",
            {"phone_number_id": phone_number_id},
        )
    )


@mcp.tool()
def search_sms(
    query: str,
    phone_number_id: Optional[str] = None,
    limit: int = 20,
) -> str:
    """Semantic search across SMS messages.

    Returns matching messages with body, direction, and timestamps.

    Args:
        query: Search query (searches message content)
        phone_number_id: Scope search to a specific phone number (optional)
        limit: Max results, 1-100 (default: 20)
    """
    params: dict[str, Any] = {"q": query, "limit": limit}
    if phone_number_id:
        params["phone_number_id"] = phone_number_id
    return _fmt(_get("/v1/sms/search", params))



@mcp.tool()
def set_phone_number_webhook(
    phone_number_id: str,
    endpoint: str,
    secret: Optional[str] = None,
    events: Optional[list[str]] = None,
) -> str:
    """Configure a webhook for a phone number to receive SMS event notifications.

    When set, Commune will POST to your endpoint on sms.received and sms.sent events.

    Args:
        phone_number_id: The phone number ID (from list_phone_numbers)
        endpoint: HTTPS URL to receive webhook payloads
        secret: Optional webhook signing secret for payload verification
        events: Event types to subscribe to (default: ["sms.received", "sms.sent"])
    """
    webhook: dict[str, Any] = {"endpoint": endpoint}
    if secret is not None:
        webhook["secret"] = secret
    if events is not None:
        webhook["events"] = events
    resp = httpx.patch(
        f"{BASE_URL}/v1/phone-numbers/{phone_number_id}",
        headers=_headers(),
        json={"webhook": webhook},
        timeout=30,
    )
    resp.raise_for_status()
    body = resp.json()
    return _fmt(body.get("data", body) if isinstance(body, dict) else body)


@mcp.tool()
def set_phone_number_allow_list(
    phone_number_id: str,
    numbers: list[str],
) -> str:
    """Set the allow list for a phone number (only these numbers can send SMS to it).

    Replaces the existing allow list. Pass an empty list to clear it.

    Args:
        phone_number_id: The phone number ID (from list_phone_numbers)
        numbers: List of E.164 phone numbers to allow, e.g. ["+15551234567"]
    """
    return _fmt(_put(f"/v1/phone-numbers/{phone_number_id}/allow-list", {"numbers": numbers}))


@mcp.tool()
def set_phone_number_block_list(
    phone_number_id: str,
    numbers: list[str],
) -> str:
    """Set the block list for a phone number (these numbers are rejected).

    Replaces the existing block list. Pass an empty list to clear it.

    Args:
        phone_number_id: The phone number ID (from list_phone_numbers)
        numbers: List of E.164 phone numbers to block, e.g. ["+15551234567"]
    """
    return _fmt(_put(f"/v1/phone-numbers/{phone_number_id}/block-list", {"numbers": numbers}))


@mcp.tool()
def list_sms_suppressions(
    phone_number_id: Optional[str] = None,
) -> str:
    """List phone numbers suppressed from receiving SMS (opted out via STOP keyword).

    Args:
        phone_number_id: Filter by phone number ID (optional, lists all if omitted)
    """
    params: dict[str, Any] = {}
    if phone_number_id:
        params["phone_number_id"] = phone_number_id
    return _fmt(_get("/v1/sms/suppressions", params))


@mcp.tool()
def remove_sms_suppression(phone_number: str) -> str:
    """Remove a phone number from the SMS suppression list (re-enable SMS delivery).

    Use this when a user sends UNSTOP or contacts support to re-subscribe.

    Args:
        phone_number: The E.164 phone number to remove from suppressions, e.g. "+15551234567"
    """
    from urllib.parse import quote
    encoded = quote(phone_number, safe="")
    return _fmt(_delete(f"/v1/sms/suppressions/{encoded}"))


# ═════════════════════════════════════════════════════════════════════════════
# CREDITS TOOLS
# ═════════════════════════════════════════════════════════════════════════════


@mcp.tool()
def get_credit_balance() -> str:
    """Get current credit balance for your Commune account.

    Returns included credits, purchased credits, total available, and credits used this billing cycle.
    """
    return _fmt(_get("/v1/credits"))


@mcp.tool()
def list_credit_bundles() -> str:
    """List available credit bundles that can be purchased.

    Returns each bundle's ID, credit amount, price, and description.
    """
    return _fmt(_get("/v1/credits/bundles"))


@mcp.tool()
def credits_checkout(bundle: str, return_url: Optional[str] = None) -> str:
    """Create a Stripe checkout session to purchase a credit bundle.

    Returns a checkout_url to open in the browser to complete payment.
    Valid bundles: "starter", "growth", "scale".

    Args:
        bundle: The bundle ID to purchase — "starter", "growth", or "scale"
        return_url: URL to redirect to after payment (optional)
    """
    payload: dict[str, Any] = {"bundle": bundle}
    if return_url:
        payload["return_url"] = return_url
    return _fmt(_post("/v1/credits/checkout", payload))


# ─── Feedback ────────────────────────────────────────────────────────────────


@mcp.tool()
def submit_feedback(
    type: Literal["error", "feature", "signal"],
    message: str,
    context: Optional[dict[str, Any]] = None,
) -> str:
    """Submit feedback about Commune to the product team.

    Use this tool whenever you encounter something worth reporting — an error,
    a missing capability, or an observation about what's working well or could
    be improved.

    Three feedback types:

      "error"   — the API returned a non-2xx status, the response shape was
                  wrong, or behavior was incorrect (not just unexpected).
                  Include context: which tool you called, what you expected,
                  what happened. Example: "list_threads returns 500 when the
                  inbox has never received a message."

      "feature" — you need a capability that doesn't exist yet.
                  Be specific about the use case. Example: "Need cursor-based
                  pagination on search_threads — current limit cap blocks
                  processing large backlogs."

      "signal"  — the API succeeded but quality, latency, or behavior was
                  unexpected. Also use for positive observations.
                  Example: "Semantic search quality on long threads is
                  excellent — intent matching across 40+ messages works."
                  Example: "Thread metadata endpoint is slow (~3s) on inboxes
                  with 1000+ threads — expected <500ms."

    The optional context dict lets you attach structured metadata that makes
    feedback actionable. For errors, include the tool name, any IDs, and
    status codes. For features, include the related tool and your use case.

    Args:
        type: Feedback type — "error", "feature", or "signal"
        message: Clear description of the feedback (max 4000 chars)
        context: Optional structured metadata, e.g.
                 {"tool": "list_threads", "inbox_id": "inb_123", "status_code": 500}
    """
    payload: dict[str, Any] = {"type": type, "message": message}
    if context:
        payload["context"] = context
    return _fmt(_post("/v1/feedback", payload))


# ═════════════════════════════════════════════════════════════════════════════
# ENTRY POINT
# ═════════════════════════════════════════════════════════════════════════════

def main():
    """Entry point for the Commune MCP server."""
    # Handle --version flag
    if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-v", "version"):
        print(f"commune-mcp {MCP_VERSION} (API: {API_VERSION})")
        sys.exit(0)

    api_key = os.environ.get("COMMUNE_API_KEY", "")
    if not api_key:
        print(
            "Error: Set the COMMUNE_API_KEY environment variable.\n"
            "  export COMMUNE_API_KEY=comm_...",
            file=sys.stderr,
        )
        sys.exit(1)

    _api_key_ctx.set(api_key)
    mcp.run()


if __name__ == "__main__":
    main()
