import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Union

from app.application.messaging.interaction import InteractionContext, MessageGateway
from app.runtime.events import EventManager
from app.schemas.message import Message
from app.schemas.types import EventType, NotificationChannel


@dataclass
class PendingPluginInputInteraction:
    """
    记录插件临时接管用户下一条文本输入的会话。
    """

    request_id: str
    user_id: str
    plugin_id: str
    channel: Optional[NotificationChannel]
    source: Optional[str]
    username: Optional[str]
    chat_id: Optional[str] = None
    prompt_id: Optional[str] = None
    payload: Optional[Any] = None
    timeout_seconds: int = 120
    created_at: datetime = field(default_factory=datetime.now)
    # Optional reply binding for channels that can report reply_to_message_id.
    prompt_message_id: Optional[str] = None

    @property
    def expires_at(self) -> datetime:
        """返回输入会话的绝对过期时间。"""
        return self.created_at + timedelta(seconds=max(1, self.timeout_seconds))


class PluginInputInteractionManager:
    """
    管理插件输入会话。

    会话按用户和渠道绑定；同一用户在同一渠道只保留一个待输入会话。
    """

    EXPIRED_GRACE_SECONDS = 300

    def __init__(self):
        """初始化活动输入会话、用户渠道索引和过期墓碑。"""
        self._by_id: Dict[str, PendingPluginInputInteraction] = {}
        self._by_user_channel: Dict[Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]], str] = {}
        self._expired_by_user_channel: Dict[
            Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]],
            PendingPluginInputInteraction,
        ] = {}
        self._lock = Lock()

    @staticmethod
    def _user_channel_source_key(
            user_id: Union[str, int],
            channel: Optional[NotificationChannel],
            source: Optional[str] = None,
            chat_id: Optional[Union[str, int]] = None,
    ) -> Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]]:
        """归一化用户、渠道、来源和会话 ID 的联合索引键。"""
        return str(user_id), channel, source, str(chat_id) if chat_id not in (None, "") else None

    @classmethod
    def _keys_overlap(
            cls,
            left: Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]],
            right: Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]],
    ) -> bool:
        """判断两个输入会话键是否会争用同一条用户回复。"""
        left_user, left_channel, left_source, left_chat_id = left
        right_user, right_channel, right_source, right_chat_id = right
        if left_user != right_user:
            return False
        if left_chat_id and right_chat_id and left_chat_id != right_chat_id:
            return False
        if (left_channel is None and left_source is None) or (right_channel is None and right_source is None):
            return left_channel == right_channel and left_source == right_source
        channel_overlap = left_channel == right_channel or left_channel is None or right_channel is None
        source_overlap = left_source == right_source or left_source is None or right_source is None
        return channel_overlap and source_overlap

    def _cleanup_locked(self) -> None:
        """在持锁状态下淘汰过期会话并维护短期过期墓碑。"""
        now = datetime.now()
        expired_tombstones = [
            key
            for key, request in self._expired_by_user_channel.items()
            if request.expires_at + timedelta(seconds=self.EXPIRED_GRACE_SECONDS) < now
        ]
        for key in expired_tombstones:
            self._expired_by_user_channel.pop(key, None)

        expired = [
            request_id
            for request_id, request in self._by_id.items()
            if request.expires_at < now
        ]
        for request_id in expired:
            request = self._by_id.pop(request_id, None)
            if request:
                key = self._user_channel_source_key(
                    request.user_id,
                    request.channel,
                    request.source,
                    request.chat_id,
                )
                self._by_user_channel.pop(key, None)
                self._expired_by_user_channel[key] = request

    def create_or_replace(
            self,
            user_id: Union[str, int],
            plugin_id: str,
            channel: Optional[NotificationChannel],
            source: Optional[str],
            username: Optional[str],
            chat_id: Optional[Union[str, int]] = None,
            prompt_id: Optional[str] = None,
            timeout_seconds: int = 120,
            payload: Optional[Any] = None,
            *,
            prompt_message_id: Optional[Union[str, int]] = None,
    ) -> PendingPluginInputInteraction:
        """创建插件输入会话并替换键范围重叠的旧会话。"""
        with self._lock:
            self._cleanup_locked()
            key = self._user_channel_source_key(user_id, channel, source, chat_id)
            old_request_ids = [
                request_id
                for stored_key, request_id in self._by_user_channel.items()
                if self._keys_overlap(stored_key, key)
            ]
            for old_request_id in old_request_ids:
                self._by_id.pop(old_request_id, None)
            self._by_user_channel = {
                stored_key: request_id
                for stored_key, request_id in self._by_user_channel.items()
                if request_id not in old_request_ids
            }
            self._expired_by_user_channel = {
                stored_key: request
                for stored_key, request in self._expired_by_user_channel.items()
                if not self._keys_overlap(stored_key, key)
            }

            normalized_chat_id = str(chat_id) if chat_id not in (None, "") else None
            normalized_prompt_message_id = (
                str(prompt_message_id)
                if channel == NotificationChannel.Telegram and normalized_chat_id and prompt_message_id not in (None, "")
                else None
            )

            request = PendingPluginInputInteraction(
                request_id=uuid.uuid4().hex[:12],
                user_id=str(user_id),
                plugin_id=plugin_id,
                channel=channel,
                source=source,
                username=username,
                chat_id=normalized_chat_id,
                prompt_id=prompt_id,
                prompt_message_id=normalized_prompt_message_id,
                timeout_seconds=timeout_seconds,
                payload=payload,
            )
            self._by_id[request.request_id] = request
            self._by_user_channel[key] = request.request_id
            return request

    def get_by_user(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel] = None,
            source: Optional[str] = None,
            chat_id: Optional[Union[str, int]] = None,
    ) -> Optional[PendingPluginInputInteraction]:
        """按用户和渠道上下文查询活动输入会话。"""
        with self._lock:
            self._cleanup_locked()
            request_id = self._find_request_id_locked(user_id, channel, source, chat_id)
            if request_id:
                return self._by_id.get(request_id)
            return None

    def pop_by_user(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel] = None,
            source: Optional[str] = None,
            chat_id: Optional[Union[str, int]] = None,
    ) -> Optional[PendingPluginInputInteraction]:
        """取出并删除活动或刚过期的输入会话。"""
        with self._lock:
            self._cleanup_locked()
            key, request_id = self._find_key_and_request_id_locked(user_id, channel, source, chat_id)
            if request_id:
                self._by_user_channel.pop(key, None)
                return self._by_id.pop(request_id, None)
            expired_key, request = self._find_expired_key_and_request_locked(user_id, channel, source, chat_id)
            if expired_key:
                self._expired_by_user_channel.pop(expired_key, None)
            return request

    def consume_by_user(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel] = None,
            source: Optional[str] = None,
            chat_id: Optional[Union[str, int]] = None,
            *,
            reply_to_message_id: Optional[Union[str, int]] = None,
            bypass_reply_check: bool = False,
    ) -> Tuple[Optional[PendingPluginInputInteraction], Optional[str]]:
        """消费匹配回复的输入会话，并返回 active 或 expired 状态。"""
        with self._lock:
            key, request_id = self._find_key_and_request_id_locked(user_id, channel, source, chat_id)

            if request_id:
                request = self._by_id.get(request_id)
                if not request:
                    self._by_user_channel.pop(key, None)
                elif request.expires_at < datetime.now():
                    self._by_user_channel.pop(key, None)
                    self._by_id.pop(request_id, None)
                    if request.prompt_message_id:
                        return None, None
                    return request, "expired"
                elif not self._reply_matches_prompt(
                        request,
                        chat_id,
                        reply_to_message_id,
                        ignore_reply_to_message_id=bypass_reply_check,
                ):
                    return None, None
                else:
                    self._by_user_channel.pop(key, None)
                    self._by_id.pop(request_id, None)
                    return request, "active"
            self._cleanup_locked()
            key, request = self._find_expired_key_and_request_locked(user_id, channel, source, chat_id)
            if request:
                self._expired_by_user_channel.pop(key, None)
                if request.prompt_message_id:
                    return None, None
                return request, "expired"
            self._cleanup_locked()
            return None, None

    @staticmethod
    def _reply_matches_prompt(
            request: PendingPluginInputInteraction,
            chat_id: Optional[Union[str, int]],
            reply_to_message_id: Optional[Union[str, int]],
            *,
            ignore_reply_to_message_id: bool = False,
    ) -> bool:
        """校验消息回复关系是否绑定到原始提示。"""
        if not request.prompt_message_id:
            return True
        if not request.chat_id or chat_id in (None, ""):
            return False
        if str(chat_id) != str(request.chat_id):
            return False
        if ignore_reply_to_message_id:
            return True
        if reply_to_message_id in (None, ""):
            return False
        return str(reply_to_message_id) == str(request.prompt_message_id)

    def _find_request_id_locked(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel],
            source: Optional[str],
            chat_id: Optional[Union[str, int]] = None,
    ) -> Optional[str]:
        """在持锁状态下查找活动请求 ID。"""
        _, request_id = self._find_key_and_request_id_locked(user_id, channel, source, chat_id)
        return request_id

    def _find_key_and_request_id_locked(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel],
            source: Optional[str],
            chat_id: Optional[Union[str, int]] = None,
    ) -> Tuple[Optional[Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]]], Optional[str]]:
        """返回首个候选键及其活动请求 ID。"""
        for key in self._candidate_keys(user_id, channel, source, chat_id):
            request_id = self._by_user_channel.get(key)
            if request_id:
                return key, request_id
        return None, None

    def _find_expired_key_and_request_locked(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel],
            source: Optional[str],
            chat_id: Optional[Union[str, int]] = None,
    ) -> Tuple[Optional[Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]]],
               Optional[PendingPluginInputInteraction]]:
        """返回仍在宽限期内的过期会话及其索引键。"""
        now = datetime.now()
        for key in self._candidate_keys(user_id, channel, source, chat_id):
            request = self._expired_by_user_channel.get(key)
            if not request:
                continue
            if request.expires_at + timedelta(seconds=self.EXPIRED_GRACE_SECONDS) < now:
                self._expired_by_user_channel.pop(key, None)
                continue
            return key, request
        return None, None

    def _candidate_keys(
            self,
            user_id: Union[str, int],
            channel: Optional[NotificationChannel],
            source: Optional[str],
            chat_id: Optional[Union[str, int]] = None,
    ) -> List[Tuple[str, Optional[NotificationChannel], Optional[str], Optional[str]]]:
        """按精确到宽松顺序生成输入会话候选键。"""
        chat_key = str(chat_id) if chat_id not in (None, "") else None
        candidates = [
            self._user_channel_source_key(user_id, channel, source, chat_key),
        ]
        if source is not None:
            candidates.append(self._user_channel_source_key(user_id, channel, None, chat_key))
        if channel is not None and source is not None:
            candidates.append(self._user_channel_source_key(user_id, None, source, chat_key))
        if channel is None and source is None:
            wildcard_key = self._user_channel_source_key(user_id, None, None, chat_key)
            candidates.append(wildcard_key)
        if chat_key is not None:
            candidates.append(self._user_channel_source_key(user_id, channel, source, None))
            if source is not None:
                candidates.append(self._user_channel_source_key(user_id, channel, None, None))
            if channel is not None and source is not None:
                candidates.append(self._user_channel_source_key(user_id, None, source, None))
            if channel is None and source is None:
                candidates.append(self._user_channel_source_key(user_id, None, None, None))
        return candidates

    def remove(self, request_id: str) -> None:
        """删除指定插件输入会话及其联合索引。"""
        with self._lock:
            request = self._by_id.pop(request_id, None)
            if request:
                self._by_user_channel.pop(
                    self._user_channel_source_key(request.user_id, request.channel, request.source, request.chat_id),
                    None,
                )

    def clear(self) -> None:
        """清空活动和过期的插件输入会话。"""
        with self._lock:
            self._by_id.clear()
            self._by_user_channel.clear()
            self._expired_by_user_channel.clear()


plugin_input_interaction_manager = PluginInputInteractionManager()


class PluginInputInteractionHandler:
    """消费插件申请接管的下一条用户文本输入。"""

    def __init__(self, messenger: MessageGateway):
        """保存消息投递接口。"""
        self._messenger = messenger

    def handle_text(
            self,
            *,
            context: InteractionContext,
            text: str,
            reply_to_message_id: Optional[Union[str, int]] = None,
            images=None,
            audio_refs=None,
            files=None,
            has_audio_input: bool = False,
    ) -> bool:
        """消费插件输入会话，并派发 MessageAction 事件。"""
        if not text or not text.strip() or images or audio_refs or files or has_audio_input:
            return False
        if text.startswith("CALLBACK:"):
            return False

        channel = context.channel
        source = context.source
        userid = context.user_id
        username = context.username
        original_chat_id = context.original_chat_id

        is_cancel_text = text.strip().lower() in {"取消", "退出", "q", "quit", "exit"}
        request, status = plugin_input_interaction_manager.consume_by_user(
            userid,
            channel,
            source,
            original_chat_id,
            reply_to_message_id=reply_to_message_id,
            bypass_reply_check=is_cancel_text,
        )
        if not request:
            return False

        if status == "expired":
            # 调用时解析单例，避免模块级绑定在单例注册表被重置后与宿主脱钩
            EventManager().send_event(
                EventType.MessageAction,
                {
                    "plugin_id": request.plugin_id,
                    "__mp_target_plugin_id": request.plugin_id,
                    "text": f"plugin_input_expired|{request.request_id}",
                    "userid": userid,
                    "channel": channel,
                    "source": source,
                    "username": username,
                    "chat_id": original_chat_id,
                    "reply_to_message_id": reply_to_message_id,
                    "prompt_id": request.prompt_id,
                    "input_session_id": request.request_id,
                    "expired": True,
                    "payload": request.payload,
                },
            )
            self._messenger.post_message(
                Message(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title="插件输入已超时，请重新发起操作。",
                    save_history=False,
                )
            )
            return not text.strip().startswith("/")

        if is_cancel_text:
            EventManager().send_event(
                EventType.MessageAction,
                {
                    "plugin_id": request.plugin_id,
                    "__mp_target_plugin_id": request.plugin_id,
                    "text": f"plugin_input_cancel|{request.request_id}",
                    "userid": userid,
                    "channel": channel,
                    "source": source,
                    "username": username,
                    "chat_id": original_chat_id,
                    "reply_to_message_id": reply_to_message_id,
                    "prompt_id": request.prompt_id,
                    "input_session_id": request.request_id,
                    "cancelled": True,
                    "payload": request.payload,
                },
            )
            self._messenger.post_message(
                Message(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title="已取消插件输入",
                    save_history=False,
                )
            )
            return True

        EventManager().send_event(
            EventType.MessageAction,
            {
                "plugin_id": request.plugin_id,
                "__mp_target_plugin_id": request.plugin_id,
                "text": f"plugin_input|{request.request_id}",
                "input_text": text,
                "userid": userid,
                "channel": channel,
                "source": source,
                "username": username,
                "chat_id": original_chat_id,
                "reply_to_message_id": reply_to_message_id,
                "prompt_id": request.prompt_id,
                "input_session_id": request.request_id,
                "payload": request.payload,
            },
        )
        return True

    @staticmethod
    def parse_callback(callback_data: str) -> Optional[Tuple[str, str]]:
        """解析插件按钮回调，格式错误时返回 None。"""
        if not callback_data.startswith("[PLUGIN]"):
            return None
        # 用 partition 避免缺少分隔符的回调抛异常
        plugin_id, separator, content = callback_data.partition("|")
        if not separator:
            return None
        return plugin_id.replace("[PLUGIN]", "", 1), content
