"""
Async Slack Web API Client.

Production-quality client with:
- Persistent connection pooling (httpx.AsyncClient)
- Automatic retry with exponential backoff (429, 5xx)
- Rate limit handling (Retry-After header)
- Structured error types
- Proper resource cleanup
"""

import asyncio
import logging
import sys
from typing import Any, Optional

import httpx

logger = logging.getLogger("slack_mcp_server.client")

# Configure logging to stderr (required for stdio MCP transport)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
    stream=sys.stderr,
)


# --- Error Types ---

class SlackAPIError(Exception):
    """Base error for Slack API failures."""

    def __init__(self, error: str, message: str = ""):
        self.error = error
        self.message = message or error
        super().__init__(self.message)


class SlackAuthError(SlackAPIError):
    """Authentication/authorization failure."""
    pass


class SlackNotFoundError(SlackAPIError):
    """Resource not found (channel, user, etc.)."""
    pass


class SlackRateLimitError(SlackAPIError):
    """Rate limit exceeded — caller should retry after delay."""

    def __init__(self, retry_after: int = 30):
        self.retry_after = retry_after
        super().__init__("ratelimited", f"Rate limited. Retry after {retry_after}s")


# --- Client ---

class SlackClient:
    """Async Slack Web API client with connection pooling and retry."""

    BASE_URL = "https://slack.com/api"
    MAX_RETRIES = 3
    BACKOFF_BASE = 1.0  # seconds

    AUTH_ERRORS = frozenset({"not_authed", "invalid_auth", "token_revoked", "token_expired", "account_inactive"})
    NOT_FOUND_ERRORS = frozenset({"channel_not_found", "user_not_found", "message_not_found", "file_not_found"})

    def __init__(self, bot_token: str, user_token: str = ""):
        if not bot_token:
            raise SlackAuthError("no_token", "SLACK_BOT_TOKEN is required")
        self.bot_token = bot_token
        self.user_token = user_token or bot_token

        self._bot_client = httpx.AsyncClient(
            base_url=self.BASE_URL,
            headers={
                "Authorization": f"Bearer {bot_token}",
                "Content-Type": "application/json; charset=utf-8",
            },
            timeout=30.0,
        )
        self._user_client = httpx.AsyncClient(
            base_url=self.BASE_URL,
            headers={
                "Authorization": f"Bearer {self.user_token}",
                "Content-Type": "application/json; charset=utf-8",
            },
            timeout=30.0,
        ) if user_token else self._bot_client

    async def close(self):
        """Close underlying HTTP clients."""
        await self._bot_client.aclose()
        if self._user_client is not self._bot_client:
            await self._user_client.aclose()

    async def __aenter__(self):
        return self

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

    async def _request(
        self,
        method: str,
        endpoint: str,
        use_user_token: bool = False,
        **kwargs,
    ) -> dict[str, Any]:
        """Make an authenticated request with retry logic."""
        client = self._user_client if use_user_token else self._bot_client

        for attempt in range(self.MAX_RETRIES):
            try:
                if method == "GET":
                    resp = await client.get(f"/{endpoint}", params=kwargs.get("params"))
                elif method == "POST":
                    if "data" in kwargs:
                        resp = await client.post(f"/{endpoint}", data=kwargs["data"], files=kwargs.get("files"))
                    else:
                        resp = await client.post(f"/{endpoint}", json=kwargs.get("json"))
                else:
                    raise ValueError(f"Unsupported method: {method}")

                if resp.status_code == 429:
                    retry_after = int(resp.headers.get("Retry-After", 30))
                    if attempt < self.MAX_RETRIES - 1:
                        logger.warning("Rate limited, retrying after %ds (attempt %d/%d)", retry_after, attempt + 1, self.MAX_RETRIES)
                        await asyncio.sleep(min(retry_after, 60))
                        continue
                    raise SlackRateLimitError(retry_after)

                if resp.status_code >= 500:
                    if attempt < self.MAX_RETRIES - 1:
                        delay = self.BACKOFF_BASE * (2 ** attempt)
                        logger.warning("Server error %d, retrying in %.1fs (attempt %d/%d)", resp.status_code, delay, attempt + 1, self.MAX_RETRIES)
                        await asyncio.sleep(delay)
                        continue

                data = resp.json()

                if not data.get("ok"):
                    error = data.get("error", "unknown_error")
                    if error in self.AUTH_ERRORS:
                        raise SlackAuthError(error)
                    elif error in self.NOT_FOUND_ERRORS:
                        raise SlackNotFoundError(error)
                    else:
                        raise SlackAPIError(error, data.get("detail", error))

                return data

            except httpx.RequestError as e:
                if attempt < self.MAX_RETRIES - 1:
                    delay = self.BACKOFF_BASE * (2 ** attempt)
                    logger.warning("Request error: %s, retrying in %.1fs", str(e), delay)
                    await asyncio.sleep(delay)
                    continue
                raise SlackAPIError("request_error", str(e))

        raise SlackAPIError("max_retries_exceeded", "Failed after maximum retries")

    # --- Channel Resolution ---

    async def resolve_channel(self, channel: str) -> str:
        """Resolve a channel name to its ID. If already an ID (starts with C/G/D), return as-is."""
        if channel.startswith(("C", "G", "D")) and len(channel) >= 9:
            return channel
        # Strip # prefix if present
        name = channel.lstrip("#").lower()
        channels = await self.list_channels(types="public_channel,private_channel")
        for ch in channels:
            if ch["name"].lower() == name:
                return ch["id"]
        raise SlackNotFoundError("channel_not_found", f"Channel '{channel}' not found")

    # --- Messaging ---

    async def post_message(self, channel: str, text: str, thread_ts: Optional[str] = None) -> dict:
        """Post a message to a channel."""
        channel_id = await self.resolve_channel(channel)
        payload: dict[str, Any] = {"channel": channel_id, "text": text}
        if thread_ts:
            payload["thread_ts"] = thread_ts
        data = await self._request("POST", "chat.postMessage", json=payload)
        return {
            "channel": data.get("channel", channel_id),
            "ts": data.get("ts", ""),
            "permalink": data.get("permalink", ""),
        }

    async def get_messages(self, channel: str, limit: int = 10) -> list[dict]:
        """Get recent messages from a channel."""
        channel_id = await self.resolve_channel(channel)
        data = await self._request("GET", "conversations.history", params={"channel": channel_id, "limit": limit})
        return [
            {
                "user": msg.get("user", "unknown"),
                "text": msg.get("text", ""),
                "timestamp": msg.get("ts", ""),
                "type": msg.get("subtype", "message"),
            }
            for msg in data.get("messages", [])
        ]

    # --- Channels ---

    async def list_channels(self, types: str = "public_channel") -> list[dict]:
        """List workspace channels."""
        data = await self._request("GET", "conversations.list", params={"types": types, "limit": 200, "exclude_archived": True})
        return [
            {
                "id": ch["id"],
                "name": ch.get("name", ""),
                "topic": ch.get("topic", {}).get("value", ""),
                "purpose": ch.get("purpose", {}).get("value", ""),
                "num_members": ch.get("num_members", 0),
                "is_private": ch.get("is_private", False),
            }
            for ch in data.get("channels", [])
        ]

    async def create_channel(self, name: str, purpose: str = "") -> dict:
        """Create a new channel."""
        data = await self._request("POST", "conversations.create", json={"name": name})
        channel = data.get("channel", {})
        if purpose:
            await self._request("POST", "conversations.setPurpose", json={"channel": channel["id"], "purpose": purpose})
        return {"channel_id": channel["id"], "name": channel.get("name", name)}

    async def archive_channel(self, channel: str) -> dict:
        """Archive a channel."""
        channel_id = await self.resolve_channel(channel)
        await self._request("POST", "conversations.archive", json={"channel": channel_id})
        return {"archived": True, "channel": channel_id}

    async def set_channel_topic(self, channel: str, topic: str) -> dict:
        """Set a channel's topic."""
        channel_id = await self.resolve_channel(channel)
        data = await self._request("POST", "conversations.setTopic", json={"channel": channel_id, "topic": topic})
        return {"channel": channel_id, "topic": data.get("topic", topic)}

    # --- Search ---

    async def search_messages(self, query: str, count: int = 10) -> list[dict]:
        """Search messages (uses user token for broader results)."""
        data = await self._request(
            "GET", "search.messages",
            use_user_token=True,
            params={"query": query, "count": count, "sort": "timestamp"},
        )
        return [
            {
                "text": match.get("text", ""),
                "channel": match.get("channel", {}).get("name", ""),
                "user": match.get("username", "unknown"),
                "timestamp": match.get("ts", ""),
                "permalink": match.get("permalink", ""),
            }
            for match in data.get("messages", {}).get("matches", [])
        ]

    # --- Reactions ---

    async def add_reaction(self, channel: str, timestamp: str, name: str) -> dict:
        """Add an emoji reaction to a message."""
        await self._request("POST", "reactions.add", json={"channel": channel, "timestamp": timestamp, "name": name})
        return {"added": True, "emoji": name}

    async def remove_reaction(self, channel: str, timestamp: str, name: str) -> dict:
        """Remove an emoji reaction from a message."""
        await self._request("POST", "reactions.remove", json={"channel": channel, "timestamp": timestamp, "name": name})
        return {"removed": True, "emoji": name}

    # --- Pins ---

    async def pin_message(self, channel: str, timestamp: str) -> dict:
        """Pin a message in a channel."""
        await self._request("POST", "pins.add", json={"channel": channel, "timestamp": timestamp})
        return {"pinned": True, "channel": channel, "timestamp": timestamp}

    async def unpin_message(self, channel: str, timestamp: str) -> dict:
        """Unpin a message from a channel."""
        await self._request("POST", "pins.remove", json={"channel": channel, "timestamp": timestamp})
        return {"unpinned": True, "channel": channel, "timestamp": timestamp}

    # --- Users ---

    async def get_user_profile(self, user_id: str) -> dict:
        """Get a user's profile information."""
        data = await self._request("GET", "users.info", params={"user": user_id})
        user = data.get("user", {})
        profile = user.get("profile", {})
        return {
            "id": user.get("id", user_id),
            "name": user.get("name", ""),
            "real_name": user.get("real_name", profile.get("real_name", "")),
            "display_name": profile.get("display_name", ""),
            "email": profile.get("email", ""),
            "title": profile.get("title", ""),
            "status_text": profile.get("status_text", ""),
            "status_emoji": profile.get("status_emoji", ""),
            "is_admin": user.get("is_admin", False),
            "timezone": user.get("tz", ""),
        }

    # --- Files ---

    async def upload_file(self, channels: str, content: str, filename: str, title: str = "") -> dict:
        """Upload a text file to a channel."""
        data = await self._request(
            "POST", "files.upload",
            data={
                "channels": channels,
                "content": content,
                "filename": filename,
                "title": title or filename,
            },
        )
        file_info = data.get("file", {})
        return {
            "file_id": file_info.get("id", ""),
            "filename": file_info.get("name", filename),
            "url": file_info.get("permalink", ""),
        }

    # --- DMs ---

    async def open_dm(self, user_id: str) -> str:
        """Open a DM channel with a user, returns channel ID."""
        data = await self._request("POST", "conversations.open", json={"users": user_id})
        return data.get("channel", {}).get("id", "")

    async def send_dm(self, user_id: str, text: str) -> dict:
        """Send a direct message to a user."""
        channel_id = await self.open_dm(user_id)
        return await self.post_message(channel=channel_id, text=text)
