"""
Service clients for credential-backed API access.

This module provides service-specific clients that automations use to
interact with external services. Clients are obtained via CredentialProxy.get_client().

All clients follow the same pattern:
- Pre-authenticated (token management is automatic)
- High-level API methods for common operations
- Consistent error handling
"""

import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from email.utils import parsedate_to_datetime
from typing import Any

import httpx

from torivers_sdk.context.credentials import (
    CredentialError,
    CredentialNotAllowedError,
    CredentialNotConfiguredError,
)


class ServiceClient(ABC):
    """
    Abstract base class for service-specific clients.

    Each service (Gmail, Slack, etc.) has its own client implementation
    that provides a high-level API for interacting with the service.

    Clients are pre-authenticated - token refresh is handled transparently
    by the ToRivers runtime.
    """

    @property
    @abstractmethod
    def service_name(self) -> str:
        """Get the name of the service this client connects to."""
        pass

    @abstractmethod
    def is_authenticated(self) -> bool:
        """Check if the client has valid authentication."""
        pass


def _get_proxy_context(auth_context: dict[str, Any]) -> tuple[str, str]:
    token = (
        auth_context.get("token")
        or os.environ.get("TORIVERS_CREDENTIAL_TOKEN")
        or os.environ.get("CREDENTIALS_TOKEN")
    )
    proxy_url = (
        auth_context.get("proxy_url")
        or os.environ.get("TORIVERS_CREDENTIAL_PROXY_URL")
        or os.environ.get("CREDENTIAL_PROXY_URL")
    )

    if not token or not proxy_url:
        raise CredentialNotConfiguredError(
            "Credential proxy runtime is not configured. "
            "This method is only available inside the ToRivers runtime."
        )

    return str(proxy_url).rstrip("/"), str(token)


def _proxy_request(
    *,
    service: str,
    operation: str,
    params: dict[str, Any],
    auth_context: dict[str, Any],
    timeout_seconds: float = 60.0,
) -> dict[str, Any]:
    base_url, token = _get_proxy_context(auth_context)
    url = f"{base_url}/api/credential/{service}/{operation}"

    headers = {"Authorization": f"Bearer {token}"}
    clean_params = {k: v for k, v in params.items() if v is not None}
    payload = {"params": clean_params}

    try:
        with httpx.Client(timeout=timeout_seconds) as client:
            resp = client.post(url, json=payload, headers=headers)
    except httpx.TimeoutException as exc:
        raise CredentialError(f"Credential proxy request timed out: {exc}") from exc
    except httpx.HTTPError as exc:
        raise CredentialError(f"Credential proxy connection error: {exc}") from exc

    if resp.status_code == 200:
        data = resp.json() or {}
        return data.get("data") or {}

    detail = ""
    try:
        detail = (resp.json() or {}).get("detail", "")
    except Exception:
        detail = resp.text or ""

    if resp.status_code == 401:
        raise CredentialError(detail or "Credential proxy access denied")
    if resp.status_code == 403:
        raise CredentialNotAllowedError(detail or "Credential access not allowed")
    if resp.status_code == 404:
        raise CredentialNotConfiguredError(
            detail or "Credential is not configured for this service"
        )

    raise CredentialError(
        detail or f"Credential proxy request failed (HTTP {resp.status_code})"
    )


# =============================================================================
# Gmail Client
# =============================================================================


@dataclass
class EmailMessage:
    """Represents an email message."""

    id: str
    thread_id: str
    subject: str
    sender: str
    recipients: list[str]
    snippet: str
    date: datetime
    labels: list[str]
    body_text: str | None = None
    body_html: str | None = None


@dataclass
class EmailDraft:
    """Represents an email draft to be sent."""

    to: list[str]
    subject: str
    body: str
    cc: list[str] | None = None
    bcc: list[str] | None = None
    is_html: bool = False


class GmailClient(ServiceClient):
    """
    Client for Gmail API operations.

    Provides high-level methods for common email operations.

    Example:
        gmail = credentials.get_client("gmail")

        # Send an email
        gmail.send_email(
            to=["user@example.com"],
            subject="Hello",
            body="Hello from automation!"
        )

        # List recent emails
        emails = gmail.list_emails(max_results=10)
        for email in emails:
            print(f"{email.sender}: {email.subject}")
    """

    def __init__(self, _auth_context: dict[str, Any] | None = None) -> None:
        """
        Initialize Gmail client.

        Args:
            _auth_context: Internal authentication context (injected by runtime)
        """
        self._auth_context = _auth_context or {}
        self._authenticated = bool(_auth_context)

    @property
    def service_name(self) -> str:
        """Get the service name."""
        return "gmail"

    def is_authenticated(self) -> bool:
        """Check if authenticated."""
        return self._authenticated

    def send_email(
        self,
        to: list[str],
        subject: str,
        body: str,
        cc: list[str] | None = None,
        bcc: list[str] | None = None,
        is_html: bool = False,
    ) -> str:
        """
        Send an email.

        Args:
            to: List of recipient email addresses
            subject: Email subject
            body: Email body (plain text or HTML)
            cc: Optional CC recipients
            bcc: Optional BCC recipients
            is_html: Whether body is HTML (default: False)

        Returns:
            Message ID of sent email

        Raises:
            RuntimeError: If sending fails
        """
        plain_body = "" if is_html else body
        html_body = body if is_html else None
        data = _proxy_request(
            service="gmail",
            operation="send_email",
            params={
                "to": to,
                "subject": subject,
                "body": plain_body,
                "cc": cc,
                "bcc": bcc,
                "html_body": html_body,
            },
            auth_context=self._auth_context,
        )
        message_id = data.get("id")
        if not isinstance(message_id, str) or not message_id:
            raise CredentialError("Gmail send_email returned no message id")
        return message_id

    def list_emails(
        self,
        max_results: int = 10,
        query: str | None = None,
        label: str | None = None,
    ) -> list[EmailMessage]:
        """
        List emails matching criteria.

        Args:
            max_results: Maximum number of emails to return
            query: Gmail search query (e.g., "from:user@example.com")
            label: Filter by label (e.g., "INBOX", "SENT")

        Returns:
            List of email messages
        """
        data = _proxy_request(
            service="gmail",
            operation="list_emails",
            params={
                "max_results": max_results,
                "query": query,
                "label": label,
            },
            auth_context=self._auth_context,
        )

        messages = data.get("messages") or []
        results: list[EmailMessage] = []
        for msg in messages:
            if isinstance(msg, dict) and msg.get("id") and "payload" in msg:
                results.append(_parse_gmail_message(msg))
            elif isinstance(msg, dict) and msg.get("id"):
                results.append(self.get_email(str(msg["id"])))
        return results

    def get_email(self, message_id: str) -> EmailMessage:
        """
        Get a specific email by ID.

        Args:
            message_id: Gmail message ID

        Returns:
            Email message with full content
        """
        data = _proxy_request(
            service="gmail",
            operation="get_email",
            params={"message_id": message_id},
            auth_context=self._auth_context,
        )
        return _parse_gmail_message(data)

    def create_draft(self, draft: EmailDraft) -> str:
        """
        Create an email draft.

        Args:
            draft: Draft to create

        Returns:
            Draft ID
        """
        data = _proxy_request(
            service="gmail",
            operation="create_draft",
            params={
                "to": draft.to,
                "subject": draft.subject,
                "body": draft.body,
                "cc": draft.cc,
                "bcc": draft.bcc,
                "is_html": draft.is_html,
            },
            auth_context=self._auth_context,
        )
        draft_id = data.get("draft_id") or data.get("id")
        if not isinstance(draft_id, str) or not draft_id:
            raise CredentialError("Gmail create_draft returned no draft id")
        return draft_id


# =============================================================================
# Google Sheets Client
# =============================================================================


@dataclass
class SpreadsheetInfo:
    """Information about a Google Sheets spreadsheet."""

    spreadsheet_id: str
    title: str
    sheets: list[str]
    url: str


@dataclass
class SheetRange:
    """Represents a range of cells in a sheet."""

    sheet_name: str
    start_row: int
    start_col: int
    end_row: int
    end_col: int

    def to_a1_notation(self) -> str:
        """Convert to A1 notation (e.g., 'Sheet1!A1:C10')."""

        def col_to_letter(col: int) -> str:
            result = ""
            while col > 0:
                col, remainder = divmod(col - 1, 26)
                result = chr(65 + remainder) + result
            return result

        start = f"{col_to_letter(self.start_col)}{self.start_row}"
        end = f"{col_to_letter(self.end_col)}{self.end_row}"
        return f"{self.sheet_name}!{start}:{end}"


class GoogleSheetsClient(ServiceClient):
    """
    Client for Google Sheets API operations.

    Provides high-level methods for spreadsheet operations.

    Example:
        sheets = credentials.get_client("google_sheets")

        # Read data
        data = sheets.get_range_values(
            spreadsheet_id="abc123",
            range="Sheet1!A1:C10"
        )

        # Write data
        sheets.update_range(
            spreadsheet_id="abc123",
            range="Sheet1!A1",
            values=[["Name", "Age"], ["Alice", 30]]
        )
    """

    def __init__(self, _auth_context: dict[str, Any] | None = None) -> None:
        """
        Initialize Google Sheets client.

        Args:
            _auth_context: Internal authentication context (injected by runtime)
        """
        self._auth_context = _auth_context or {}
        self._authenticated = bool(_auth_context)

    @property
    def service_name(self) -> str:
        """Get the service name."""
        return "google_sheets"

    def is_authenticated(self) -> bool:
        """Check if authenticated."""
        return self._authenticated

    def get_spreadsheet_info(self, spreadsheet_id: str) -> SpreadsheetInfo:
        """
        Get metadata about a spreadsheet.

        Args:
            spreadsheet_id: Google Sheets spreadsheet ID

        Returns:
            Spreadsheet metadata
        """
        data = _proxy_request(
            service="google_sheets",
            operation="get_spreadsheet_info",
            params={"spreadsheet_id": spreadsheet_id},
            auth_context=self._auth_context,
        )
        return SpreadsheetInfo(
            spreadsheet_id=str(data.get("spreadsheet_id") or spreadsheet_id),
            title=str(data.get("title") or ""),
            sheets=[s for s in (data.get("sheets") or []) if isinstance(s, str)],
            url=str(data.get("url") or ""),
        )

    def get_range_values(
        self,
        spreadsheet_id: str,
        range: str,
        value_render_option: str = "FORMATTED_VALUE",
    ) -> list[list[Any]]:
        """
        Get values from a range.

        Args:
            spreadsheet_id: Google Sheets spreadsheet ID
            range: A1 notation range (e.g., "Sheet1!A1:C10")
            value_render_option: How to render values
                ("FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA")

        Returns:
            2D list of cell values
        """
        data = _proxy_request(
            service="google_sheets",
            operation="get_data",
            params={
                "spreadsheet_id": spreadsheet_id,
                "range": range,
                "value_render_option": value_render_option,
            },
            auth_context=self._auth_context,
        )
        values = data.get("values") or []
        return values if isinstance(values, list) else []

    def update_range(
        self,
        spreadsheet_id: str,
        range: str,
        values: list[list[Any]],
        value_input_option: str = "USER_ENTERED",
    ) -> int:
        """
        Update values in a range.

        Args:
            spreadsheet_id: Google Sheets spreadsheet ID
            range: A1 notation range (e.g., "Sheet1!A1")
            values: 2D list of values to write
            value_input_option: How to interpret values
                ("RAW", "USER_ENTERED")

        Returns:
            Number of cells updated
        """
        data = _proxy_request(
            service="google_sheets",
            operation="update_range",
            params={
                "spreadsheet_id": spreadsheet_id,
                "range": range,
                "values": values,
                "value_input_option": value_input_option,
            },
            auth_context=self._auth_context,
        )
        updates = data.get("updatedCells") or (data.get("updates") or {}).get(
            "updatedCells"
        )
        return int(updates) if isinstance(updates, int) else 0

    def append_rows(
        self,
        spreadsheet_id: str,
        range: str,
        values: list[list[Any]],
    ) -> int:
        """
        Append rows to a sheet.

        Args:
            spreadsheet_id: Google Sheets spreadsheet ID
            range: A1 notation range (e.g., "Sheet1!A:C")
            values: 2D list of row values to append

        Returns:
            Number of rows appended
        """
        data = _proxy_request(
            service="google_sheets",
            operation="append_rows",
            params={
                "spreadsheet_id": spreadsheet_id,
                "range": range,
                "values": values,
            },
            auth_context=self._auth_context,
        )
        updates = (data.get("updates") or {}).get("updatedRows")
        return int(updates) if isinstance(updates, int) else 0

    def clear_range(self, spreadsheet_id: str, range: str) -> None:
        """
        Clear values from a range.

        Args:
            spreadsheet_id: Google Sheets spreadsheet ID
            range: A1 notation range to clear
        """
        _proxy_request(
            service="google_sheets",
            operation="clear_range",
            params={"spreadsheet_id": spreadsheet_id, "range": range},
            auth_context=self._auth_context,
        )
        return None


# =============================================================================
# Slack Client
# =============================================================================


@dataclass
class SlackChannel:
    """Represents a Slack channel."""

    id: str
    name: str
    is_private: bool
    is_member: bool
    topic: str | None = None
    purpose: str | None = None


@dataclass
class SlackMessage:
    """Represents a Slack message."""

    ts: str  # Timestamp (message ID)
    channel: str
    text: str
    user: str | None = None
    thread_ts: str | None = None


class SlackClient(ServiceClient):
    """
    Client for Slack API operations.

    Provides high-level methods for Slack messaging.

    Example:
        slack = credentials.get_client("slack")

        # Send a message
        slack.send_message(
            channel="#general",
            text="Hello from automation!"
        )

        # Send with blocks
        slack.send_message(
            channel="#general",
            text="Fallback text",
            blocks=[{"type": "section", "text": {"type": "mrkdwn", "text": "*Bold*"}}]
        )
    """

    def __init__(self, _auth_context: dict[str, Any] | None = None) -> None:
        """
        Initialize Slack client.

        Args:
            _auth_context: Internal authentication context (injected by runtime)
        """
        self._auth_context = _auth_context or {}
        self._authenticated = bool(_auth_context)

    @property
    def service_name(self) -> str:
        """Get the service name."""
        return "slack"

    def is_authenticated(self) -> bool:
        """Check if authenticated."""
        return self._authenticated

    def send_message(
        self,
        channel: str,
        text: str,
        blocks: list[dict[str, Any]] | None = None,
        thread_ts: str | None = None,
    ) -> SlackMessage:
        """
        Send a message to a channel.

        Args:
            channel: Channel ID or name (e.g., "#general" or "C1234567890")
            text: Message text (also used as fallback for blocks)
            blocks: Optional Block Kit blocks for rich formatting
            thread_ts: Optional thread timestamp to reply in thread

        Returns:
            Sent message details
        """
        data = _proxy_request(
            service="slack",
            operation="send_message",
            params={
                "channel": channel,
                "text": text,
                "blocks": blocks,
                "thread_ts": thread_ts,
            },
            auth_context=self._auth_context,
        )
        return SlackMessage(
            ts=str(data.get("ts") or ""),
            channel=str(data.get("channel") or channel),
            text=str(data.get("text") or text),
            thread_ts=data.get("thread_ts"),
        )

    def get_channel(self, channel: str) -> SlackChannel:
        """
        Get channel information.

        Args:
            channel: Channel ID or name

        Returns:
            Channel information
        """
        data = _proxy_request(
            service="slack",
            operation="get_channel",
            params={"channel": channel},
            auth_context=self._auth_context,
        )
        return SlackChannel(
            id=str(data.get("id") or ""),
            name=str(data.get("name") or ""),
            is_private=bool(data.get("is_private", False)),
            is_member=bool(data.get("is_member", False)),
            topic=data.get("topic"),
            purpose=data.get("purpose"),
        )

    def list_channels(
        self,
        exclude_archived: bool = True,
        types: str = "public_channel,private_channel",
    ) -> list[SlackChannel]:
        """
        List available channels.

        Args:
            exclude_archived: Whether to exclude archived channels
            types: Comma-separated channel types to include

        Returns:
            List of channels
        """
        data = _proxy_request(
            service="slack",
            operation="list_channels",
            params={
                "exclude_archived": exclude_archived,
                "types": types,
            },
            auth_context=self._auth_context,
        )
        channels = data.get("channels") or []
        results: list[SlackChannel] = []
        for ch in channels:
            if not isinstance(ch, dict):
                continue
            results.append(
                SlackChannel(
                    id=str(ch.get("id") or ""),
                    name=str(ch.get("name") or ""),
                    is_private=bool(ch.get("is_private", False)),
                    is_member=bool(ch.get("is_member", False)),
                    topic=ch.get("topic"),
                    purpose=ch.get("purpose"),
                )
            )
        return results

    def update_message(
        self,
        channel: str,
        ts: str,
        text: str,
        blocks: list[dict[str, Any]] | None = None,
    ) -> SlackMessage:
        """
        Update an existing message.

        Args:
            channel: Channel ID
            ts: Message timestamp (ID)
            text: New message text
            blocks: Optional new Block Kit blocks

        Returns:
            Updated message details
        """
        data = _proxy_request(
            service="slack",
            operation="update_message",
            params={
                "channel": channel,
                "ts": ts,
                "text": text,
                "blocks": blocks,
            },
            auth_context=self._auth_context,
        )
        return SlackMessage(
            ts=str(data.get("ts") or ts),
            channel=str(data.get("channel") or channel),
            text=str(data.get("text") or text),
        )

    def add_reaction(self, channel: str, ts: str, emoji: str) -> None:
        """
        Add a reaction to a message.

        Args:
            channel: Channel ID
            ts: Message timestamp (ID)
            emoji: Emoji name without colons (e.g., "thumbsup")
        """
        _proxy_request(
            service="slack",
            operation="add_reaction",
            params={"channel": channel, "ts": ts, "emoji": emoji},
            auth_context=self._auth_context,
        )
        return None


def _extract_gmail_header(headers: list[dict[str, Any]], name: str) -> str | None:
    for h in headers:
        if not isinstance(h, dict):
            continue
        if str(h.get("name", "")).lower() == name.lower():
            value = h.get("value")
            return str(value) if value is not None else None
    return None


def _decode_gmail_body(data: str) -> str:
    # Gmail uses base64url without padding
    import base64

    padding = "=" * (-len(data) % 4)
    return base64.urlsafe_b64decode(data + padding).decode("utf-8", errors="replace")


def _extract_gmail_bodies(payload: dict[str, Any]) -> tuple[str | None, str | None]:
    text_body = None
    html_body = None

    def walk(part: dict[str, Any]) -> None:
        nonlocal text_body, html_body
        mime = str(part.get("mimeType") or "")
        body = part.get("body") or {}
        data = body.get("data")

        if isinstance(data, str) and data:
            if mime == "text/plain" and text_body is None:
                text_body = _decode_gmail_body(data)
            if mime == "text/html" and html_body is None:
                html_body = _decode_gmail_body(data)

        for child in part.get("parts") or []:
            if isinstance(child, dict):
                walk(child)

    if isinstance(payload, dict):
        walk(payload)

    return text_body, html_body


def _parse_gmail_message(msg: dict[str, Any]) -> EmailMessage:
    payload = msg.get("payload") or {}
    headers = payload.get("headers") or []
    headers_list = headers if isinstance(headers, list) else []

    subject = _extract_gmail_header(headers_list, "Subject") or ""
    sender = _extract_gmail_header(headers_list, "From") or ""
    to_raw = _extract_gmail_header(headers_list, "To") or ""
    recipients = [s.strip() for s in to_raw.split(",") if s.strip()]

    date_raw = _extract_gmail_header(headers_list, "Date") or ""
    try:
        date = parsedate_to_datetime(date_raw) if date_raw else datetime.utcnow()
    except Exception:
        date = datetime.utcnow()

    text_body, html_body = _extract_gmail_bodies(
        payload if isinstance(payload, dict) else {}
    )

    return EmailMessage(
        id=str(msg.get("id") or ""),
        thread_id=str(msg.get("threadId") or ""),
        subject=subject,
        sender=sender,
        recipients=recipients,
        snippet=str(msg.get("snippet") or ""),
        date=date,
        labels=[str(x) for x in (msg.get("labelIds") or []) if isinstance(x, str)],
        body_text=text_body,
        body_html=html_body,
    )


# =============================================================================
# Client Registry
# =============================================================================

# Mapping of service names to client classes
SERVICE_CLIENT_REGISTRY: dict[str, type[ServiceClient]] = {
    "gmail": GmailClient,
    "google_sheets": GoogleSheetsClient,
    "slack": SlackClient,
}


def get_client_class(service: str) -> type[ServiceClient] | None:
    """
    Get the client class for a service.

    Args:
        service: Service name

    Returns:
        Client class or None if not found
    """
    return SERVICE_CLIENT_REGISTRY.get(service)


def list_supported_services() -> list[str]:
    """
    List all supported service names.

    Returns:
        List of service names
    """
    return list(SERVICE_CLIENT_REGISTRY.keys())
