"""Custom Slack operators for reading and writing channel messages.

Provides SlackChannelReadOperator and SlackChannelWriteOperator with
built-in channel name-to-ID resolution, pagination, and error handling.
"""

from __future__ import annotations

import json
import re
from typing import Any, Sequence

from airflow.models import BaseOperator
from airflow.models.xcom_arg import XComArg
from airflow.providers.slack.hooks.slack import SlackHook
from slack_sdk.errors import SlackApiError


def _resolve_channel_id(client: Any, channel: str) -> str:
    """Resolve a channel name to its ID via paginated conversations_list.

    If *channel* already looks like an ID (starts with C, G, or D),
    it is returned as-is.
    """
    if channel and channel[0] in ("C", "G", "D"):
        return channel

    cursor = None
    while True:
        try:
            response = client.conversations_list(
                types="public_channel,private_channel",
                limit=200,
                cursor=cursor,
            )
        except SlackApiError as e:
            raise RuntimeError(
                f"Failed to list channels: {e.response['error']}"
            ) from e

        for ch in response["channels"]:
            if ch["name"] == channel:
                return ch["id"]

        next_cursor = response.get("response_metadata", {}).get("next_cursor")
        if not next_cursor:
            break
        cursor = next_cursor

    raise ValueError(f"Channel '{channel}' not found")


def _resolve_value(operator: BaseOperator, value: Any, context: dict) -> Any:
    """Resolve a value that might be an XComArg or a direct value."""
    if isinstance(value, XComArg):
        operator.log.info(f"Resolving XComArg from task: {value.operator.task_id}")
        return value.resolve(context)
    return value


class SlackChannelReadOperator(BaseOperator):
    """Read messages from a Slack channel or thread.

    Resolves channel names to IDs automatically.  Results are pushed
    to XCom as a list of message dicts containing ``text``, ``user``,
    ``ts``, ``thread_ts``, and ``reactions`` keys.
    """

    template_fields: Sequence[str] = ("channel", "oldest", "latest", "thread_ts")

    def __init__(
        self,
        *,
        channel: str,
        slack_conn_id: str = "slack_default",
        limit: int = 100,
        oldest: str | None = None,
        latest: str | None = None,
        thread_ts: str | XComArg | None = None,
        inclusive: bool = True,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        self.channel = channel
        self.slack_conn_id = slack_conn_id
        self.limit = limit
        self.oldest = oldest
        self.latest = latest
        self.thread_ts = thread_ts
        self.inclusive = inclusive

    def execute(self, context: Any) -> list[dict]:
        hook = SlackHook(slack_conn_id=self.slack_conn_id)
        client = hook.client
        channel_id = _resolve_channel_id(client, self.channel)

        thread_ts = _resolve_value(self, self.thread_ts, context)

        messages: list[dict] = []
        cursor = None

        while len(messages) < self.limit:
            kwargs: dict[str, Any] = {
                "channel": channel_id,
                "limit": min(self.limit - len(messages), 200),
                "inclusive": self.inclusive,
            }
            if self.oldest:
                kwargs["oldest"] = self.oldest
            if self.latest:
                kwargs["latest"] = self.latest
            if cursor:
                kwargs["cursor"] = cursor

            try:
                if thread_ts:
                    kwargs["ts"] = thread_ts
                    response = client.conversations_replies(**kwargs)
                else:
                    response = client.conversations_history(**kwargs)
            except SlackApiError as e:
                raise RuntimeError(
                    f"Slack API error: {e.response['error']}"
                ) from e

            for msg in response.get("messages", []):
                messages.append({
                    "text": msg.get("text", ""),
                    "user": msg.get("user", ""),
                    "ts": msg.get("ts", ""),
                    "thread_ts": msg.get("thread_ts"),
                    "reactions": msg.get("reactions", []),
                })

            next_cursor = (
                response.get("response_metadata", {}).get("next_cursor")
            )
            if not next_cursor or not response.get("has_more", False):
                break
            cursor = next_cursor

        return messages


class SlackChannelWriteOperator(BaseOperator):
    """Post, reply to, or update messages in a Slack channel with Block Kit support.

    Resolves channel names to IDs automatically. Supports XComArg/TaskFlow
    integration for blocks, thread_ts, and update_ts. The posted/updated
    message ts and channel are pushed to XCom as a dict.
    """

    template_fields: Sequence[str] = (
        "channel",
        "text",
        "blocks",
        "thread_ts",
        "update_ts",
    )
    ui_color = "#1264A3"

    def __init__(
        self,
        *,
        channel: str,
        text: str,
        slack_conn_id: str = "slack_default",
        action: str = "post",
        thread_ts: str | XComArg | None = None,
        update_ts: str | XComArg | None = None,
        blocks: list | str | XComArg | None = None,
        unfurl_links: bool = True,
        unfurl_media: bool = True,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        self.channel = channel
        self.text = text
        self.slack_conn_id = slack_conn_id
        self.action = action
        self.thread_ts = thread_ts
        self.update_ts = update_ts
        self.blocks = blocks
        self.unfurl_links = unfurl_links
        self.unfurl_media = unfurl_media

    def _parse_blocks(self, blocks: list | str | None) -> list[dict] | None:
        """Parse blocks from native list, JSON string, or markdown-fenced JSON."""
        if blocks is None:
            return None

        if isinstance(blocks, str):
            cleaned = blocks.strip()
            if cleaned.startswith('```'):
                cleaned = re.sub(r'^```json\s*', '', cleaned)
                cleaned = re.sub(r'^```\s*', '', cleaned)
                cleaned = re.sub(r'```\s*$', '', cleaned)
                cleaned = cleaned.strip()
            try:
                blocks = json.loads(cleaned)
            except json.JSONDecodeError as e:
                self.log.error(f"Failed to parse blocks as JSON: {e}")
                self.log.error(f"Blocks value: {cleaned[:200]}")
                return None

        if not isinstance(blocks, list):
            self.log.error(f"Blocks must be a list, got {type(blocks)}")
            return None

        return blocks

    def execute(self, context: Any) -> dict:
        hook = SlackHook(slack_conn_id=self.slack_conn_id)
        client = hook.client
        channel_id = _resolve_channel_id(client, self.channel)

        resolved_blocks = _resolve_value(self, self.blocks, context)
        parsed_blocks = self._parse_blocks(resolved_blocks)
        resolved_thread_ts = _resolve_value(self, self.thread_ts, context)
        resolved_update_ts = _resolve_value(self, self.update_ts, context)

        common_kwargs: dict[str, Any] = {
            "channel": channel_id,
            "text": self.text,
        }
        if parsed_blocks:
            common_kwargs["blocks"] = parsed_blocks
        common_kwargs["unfurl_links"] = self.unfurl_links
        common_kwargs["unfurl_media"] = self.unfurl_media

        try:
            if self.action == "update":
                if not resolved_update_ts:
                    raise ValueError(
                        "update_ts is required when action is 'update'"
                    )
                response = client.chat_update(
                    ts=resolved_update_ts, **common_kwargs
                )
            elif self.action == "reply":
                if not resolved_thread_ts:
                    raise ValueError(
                        "thread_ts is required when action is 'reply'"
                    )
                response = client.chat_postMessage(
                    thread_ts=resolved_thread_ts, **common_kwargs
                )
            else:
                response = client.chat_postMessage(**common_kwargs)
        except SlackApiError as e:
            raise RuntimeError(
                f"Slack API error: {e.response['error']}"
            ) from e

        return {"ts": response["ts"], "channel": response["channel"]}
