import re
from typing import List, Optional, Tuple, Union

from app.chain import ChainBase
from app.helper.interaction import (
    build_navigation_buttons,
    page_items,
    supports_interaction_buttons,
    update_or_post_message, skills_interaction_manager, PendingSkillsInteraction,
)
from app.helper.skill import SkillHelper, SkillInfo
from app.schemas import Notification
from app.schemas.types import MessageChannel


class SkillsChain(ChainBase):
    """
    处理 /skills 指令、按钮回调和文本式技能管理交互。
    """

    _button_page_size = 6
    _text_page_size = 8

    def __init__(self):
        super().__init__()
        self.skillhelper = SkillHelper()

    def remote_manage(
            self,
            arg_str: str,
            channel: MessageChannel,
            userid: Union[str, int],
            source: Optional[str] = None,
    ):
        """
        /skills 入口。创建新会话并渲染首屏菜单。
        """
        request = skills_interaction_manager.create_or_replace(
            user_id=userid,
            channel=channel,
            source=source,
            username=None,
        )
        normalized_arg = (arg_str or "").strip()
        force = normalized_arg.lower() in {"refresh", "刷新"}
        search_query = self._extract_market_search_query(normalized_arg) or (
            "" if force else normalized_arg
        )
        if search_query:
            request.view = "market"
            request.market_query = search_query
        self._render_interaction(
            request=request,
            channel=channel,
            source=source,
            userid=userid,
            username=None,
            force_market_refresh=force,
        )

    @staticmethod
    def parse_callback(callback_data: str) -> Optional[Tuple[str, str, Optional[int]]]:
        """
        解析 /skills 按钮回调。

        回调格式：skills:{request_id}:{action}[:index]
        """
        if not callback_data.startswith("skills:"):
            return None
        parts = callback_data.split(":")
        if len(parts) < 3:
            return None
        request_id = parts[1]
        action = parts[2]
        index = None
        if len(parts) >= 4 and parts[3].isdigit():
            index = int(parts[3])
        return request_id, action, index

    def handle_callback_interaction(
            self,
            callback_data: str,
            channel: MessageChannel,
            source: str,
            userid: Union[str, int],
            username: str,
            original_message_id: Optional[Union[str, int]] = None,
            original_chat_id: Optional[str] = None,
    ) -> bool:
        """
        处理按钮交互，并在同一条消息上刷新当前视图。
        """
        parsed = self.parse_callback(callback_data)
        if not parsed:
            return False

        request_id, action, index = parsed
        request = skills_interaction_manager.get_by_id(request_id, userid)
        if not request:
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title="技能交互已失效，请重新发送 /skills",
                )
            )
            return True

        request.channel = channel
        request.source = source
        request.username = username

        if action == "close":
            skills_interaction_manager.remove(request.request_id)
            self._update_or_post_message(
                channel=channel,
                source=source,
                userid=userid,
                username=username,
                title="技能管理",
                text="技能交互已结束",
                original_message_id=original_message_id,
                original_chat_id=original_chat_id,
            )
            return True

        if action == "root":
            request.view = "root"
            request.awaiting_input = None
        elif action == "installed":
            request.view = "installed"
            request.local_page = 0
            request.awaiting_input = None
        elif action == "market":
            request.view = "market"
            request.market_page = 0
            request.awaiting_input = None
        elif action == "sources":
            request.view = "sources"
            request.awaiting_input = None
        elif action == "search":
            request.view = "market"
            request.awaiting_input = "market-search"
        elif action == "source-add":
            request.view = "sources"
            request.awaiting_input = "source-add"
        elif action == "clear-search":
            self._clear_market_search(request)
        elif action == "refresh":
            request.awaiting_input = None
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
                original_message_id=original_message_id,
                original_chat_id=original_chat_id,
                force_market_refresh=True,
            )
            return True
        elif action == "page-next":
            request.awaiting_input = None
            if request.view == "installed":
                request.local_page += 1
            elif request.view == "market":
                request.market_page += 1
        elif action == "page-prev":
            request.awaiting_input = None
            if request.view == "installed":
                request.local_page = max(0, request.local_page - 1)
            elif request.view == "market":
                request.market_page = max(0, request.market_page - 1)
        elif action == "install" and index:
            request.awaiting_input = None
            success, message = self._install_market_skill(request, index)
            if success:
                self.post_message(
                    Notification(
                        channel=channel,
                        source=source,
                        userid=userid,
                        username=username,
                        title=message,
                    )
                )
            else:
                self.post_message(
                    Notification(
                        channel=channel,
                        source=source,
                        userid=userid,
                        username=username,
                        title=message,
                    )
                )
        elif action == "remove" and index:
            request.awaiting_input = None
            success, message = self._remove_local_skill(request, index)
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title=message,
                )
            )
            if not success:
                # 保持当前页
                pass
        elif action == "source-remove" and index:
            request.view = "sources"
            request.awaiting_input = None
            success, message = self._remove_market_source(index)
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title=message,
                )
            )

        self._render_interaction(
            request=request,
            channel=channel,
            source=source,
            userid=userid,
            username=username,
            original_message_id=original_message_id,
            original_chat_id=original_chat_id,
        )
        return True

    def handle_text_interaction(
            self,
            channel: MessageChannel,
            source: str,
            userid: Union[str, int],
            username: str,
            text: str,
    ) -> bool:
        """
        处理不支持按钮渠道上的文本指令，也兼容用户直接回复文字操作。
        """
        request = skills_interaction_manager.get_by_user(userid)
        if not request:
            return False

        request.channel = channel
        request.source = source
        request.username = username

        normalized = (text or "").strip()
        lowered = normalized.lower()
        if lowered in {"退出", "关闭", "q", "quit", "exit"}:
            skills_interaction_manager.remove(request.request_id)
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title="技能交互已结束",
                    save_history=False,
                )
            )
            return True

        if lowered in {"返回", "back"}:
            request.view = "root"
            request.awaiting_input = None
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        add_source = self._extract_market_source_input(normalized)
        remove_source_match = re.match(
            r"^(?:删除源|移除源|删除仓库|移除仓库|remove source)\s*(\d+)$",
            normalized,
            re.IGNORECASE,
        )

        if add_source:
            request.view = "sources"
            request.awaiting_input = None
            _, message = self.skillhelper.add_custom_market_source(add_source)
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title=message,
                )
            )
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        if remove_source_match:
            request.view = "sources"
            request.awaiting_input = None
            _, message = self._remove_market_source(
                page_index=int(remove_source_match.group(1))
            )
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title=message,
                )
            )
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        if lowered in {"刷新", "refresh"}:
            request.awaiting_input = None
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
                force_market_refresh=True,
            )
            return True

        if lowered in {"p", "prev", "上一页"}:
            request.awaiting_input = None
            if request.view == "installed":
                request.local_page = max(0, request.local_page - 1)
            elif request.view == "market":
                request.market_page = max(0, request.market_page - 1)
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        if lowered in {"n", "next", "下一页"}:
            request.awaiting_input = None
            if request.view == "installed":
                request.local_page += 1
            elif request.view == "market":
                request.market_page += 1
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        if request.view == "root":
            if lowered in {"1", "已安装", "本地", "local"}:
                request.view = "installed"
            elif lowered in {"2", "市场", "market"}:
                request.view = "market"
            elif lowered in {"3", "技能源", "源", "sources", "source"}:
                request.view = "sources"
            elif self._extract_market_search_query(normalized):
                self._apply_market_search(
                    request,
                    self._extract_market_search_query(normalized),
                )
            else:
                self.post_message(
                    Notification(
                        channel=channel,
                        source=source,
                        userid=userid,
                        username=username,
                        title="请输入 `1` 查看已安装技能，`2` 查看技能市场，3 管理技能源，或回复 `刷新/退出`",
                    )
                )
                return True
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        if request.awaiting_input == "source-add":
            if lowered in {"取消", "cancel"}:
                request.awaiting_input = None
            else:
                _, message = self.skillhelper.add_custom_market_source(normalized)
                request.awaiting_input = None
                self.post_message(
                    Notification(
                        channel=channel,
                        source=source,
                        userid=userid,
                        username=username,
                        title=message,
                    )
                )
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        if lowered in {"清除搜索", "取消搜索", "clear", "clear search"}:
            if request.view == "market" or request.market_query:
                self._clear_market_search(request)
                self._render_interaction(
                    request=request,
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                )
                return True

        if request.awaiting_input == "market-search":
            if lowered in {"取消", "cancel"}:
                request.awaiting_input = None
            else:
                search_query = self._extract_market_search_query(normalized) or normalized
                self._apply_market_search(request, search_query)
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        search_query = self._extract_market_search_query(normalized)
        if search_query:
            self._apply_market_search(request, search_query)
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        install_match = re.match(r"^(?:安装|装)\s*(\d+)$", normalized)
        remove_match = re.match(r"^(?:删除|删)\s*(\d+)$", normalized)
        if request.view == "market" and install_match:
            request.awaiting_input = None
            success, message = self._install_market_skill(
                request=request,
                page_index=int(install_match.group(1)),
            )
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title=message,
                )
            )
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True
        if request.view == "installed" and remove_match:
            request.awaiting_input = None
            _, message = self._remove_local_skill(
                request=request,
                page_index=int(remove_match.group(1)),
            )
            self.post_message(
                Notification(
                    channel=channel,
                    source=source,
                    userid=userid,
                    username=username,
                    title=message,
                )
            )
            self._render_interaction(
                request=request,
                channel=channel,
                source=source,
                userid=userid,
                username=username,
            )
            return True

        self.post_message(
            Notification(
                channel=channel,
                source=source,
                userid=userid,
                username=username,
                title=self._build_usage_hint(request.view),
            )
        )
        return True

    def _install_market_skill(
            self,
            request: PendingSkillsInteraction,
            page_index: int,
    ) -> Tuple[bool, str]:
        """
        按当前市场页的可见序号安装技能，避免跨页序号歧义。
        """
        market_skills = self._get_market_skills(request=request)
        items, page, _ = self._page_items(
            items=market_skills,
            page=request.market_page,
            page_size=self._page_size(request.channel),
        )
        request.market_page = page
        if page_index < 1 or page_index > len(items):
            return False, "安装序号无效"
        return self.skillhelper.install_market_skill(items[page_index - 1])

    def _remove_local_skill(
            self,
            request: PendingSkillsInteraction,
            page_index: int,
    ) -> Tuple[bool, str]:
        """
        按当前已安装页的可见序号删除技能，并拦截内置技能。
        """
        local_skills = self.skillhelper.list_local_skills()
        items, page, _ = self._page_items(
            items=local_skills,
            page=request.local_page,
            page_size=self._page_size(request.channel),
        )
        request.local_page = page
        if page_index < 1 or page_index > len(items):
            return False, "删除序号无效"
        target = items[page_index - 1]
        if not target.removable:
            return False, f"技能 {target.id} 是内置技能，不能删除"
        return self.skillhelper.remove_local_skill(target.id)

    def _remove_market_source(self, page_index: int) -> Tuple[bool, str]:
        """
        按当前源列表序号删除自定义技能源，避免误删内置默认源。
        """
        sources = self.skillhelper.list_market_source_entries()
        if page_index < 1 or page_index > len(sources):
            return False, "删除源序号无效"
        target = sources[page_index - 1]
        if not target.removable:
            return False, f"技能源 {target.label} 是内置默认源，不能删除"
        return self.skillhelper.remove_custom_market_source(target.source)

    def _render_interaction(
            self,
            request: PendingSkillsInteraction,
            channel: MessageChannel,
            source: Optional[str],
            userid: Union[str, int],
            username: Optional[str],
            original_message_id: Optional[Union[str, int]] = None,
            original_chat_id: Optional[str] = None,
            force_market_refresh: bool = False,
    ) -> None:
        """
        根据当前视图生成内容，并选择编辑原消息或发送新消息。
        """
        if request.view == "installed":
            title, text, buttons = self._build_installed_view(
                request=request
            )
        elif request.view == "market":
            title, text, buttons = self._build_market_view(
                request=request,
                force_market_refresh=force_market_refresh,
            )
        elif request.view == "sources":
            title, text, buttons = self._build_sources_view(
                request=request,
            )
        else:
            title, text, buttons = self._build_root_view(
                request=request,
                force_market_refresh=force_market_refresh,
            )

        self._update_or_post_message(
            channel=channel,
            source=source,
            userid=userid,
            username=username,
            title=title,
            text=text,
            buttons=buttons,
            original_message_id=original_message_id,
            original_chat_id=original_chat_id,
        )

    def _build_root_view(
            self,
            request: PendingSkillsInteraction,
            force_market_refresh: bool = False,
    ) -> Tuple[str, str, Optional[List[List[dict]]]]:
        """
        构建根菜单视图，汇总本地技能和市场概览。
        """
        local_skills = self.skillhelper.list_local_skills()
        market_skills = [
            skill
            for skill in self.skillhelper.list_market_skills(force=force_market_refresh)
            if not skill.installed
        ]
        source_entries = self.skillhelper.list_market_source_entries()
        source_lines = []
        for index, source_entry in enumerate(source_entries, start=1):
            state = "内置" if source_entry.builtin else "自定义"
            source_lines.append(
                f"{index}. {source_entry.label}（{state}）"
            )

        text_lines = [
            f"已安装技能：{len(local_skills)}",
            f"市场可安装技能：{len(market_skills)}",
        ]
        if source_lines:
            text_lines.extend(["", "当前技能源：", *source_lines])
        text_lines.extend(
            [
                "",
                "1. 查看已安装技能",
                "2. 浏览技能市场",
                "3. 管理技能源",
                "回复 `刷新` 重新获取市场数据，回复 `退出` 结束交互",
            ]
        )

        buttons = None
        if self._supports_interactive_buttons(request.channel):
            buttons = [
                [{"text": "已安装技能", "callback_data": f"skills:{request.request_id}:installed"}],
                [{"text": "技能市场", "callback_data": f"skills:{request.request_id}:market"}],
                [{"text": "技能源管理", "callback_data": f"skills:{request.request_id}:sources"}],
                [
                    {"text": "刷新市场", "callback_data": f"skills:{request.request_id}:refresh"},
                    {"text": "关闭", "callback_data": f"skills:{request.request_id}:close"},
                ],
            ]
        return "技能管理", "\n".join(text_lines), buttons

    def _build_installed_view(
            self,
            request: PendingSkillsInteraction
    ) -> Tuple[str, str, Optional[List[List[dict]]]]:
        """
        构建已安装技能视图，列出来源和可删除状态。
        """
        local_skills = self.skillhelper.list_local_skills()
        items, page, total_pages = self._page_items(
            items=local_skills,
            page=request.local_page,
            page_size=self._page_size(request.channel),
        )
        request.local_page = page

        text_lines = [f"第 {page + 1}/{total_pages} 页，共 {len(local_skills)} 个技能"]
        if not items:
            text_lines.append("")
            text_lines.append("当前没有已安装技能")
        else:
            for index, skill in enumerate(items, start=1):
                action = "可删除" if skill.removable else "内置不可删"
                text_lines.extend(
                    [
                        "",
                        f"{index}. {skill.id} ({skill.source_label}，{action})",
                        self._truncate(skill.description),
                    ]
                )

        text_lines.extend(
            [
                "",
                "回复 `删除 <序号>` 删除技能，回复 `n/p` 翻页，回复 `返回` 回到菜单，回复 `退出` 结束交互",
            ]
        )

        buttons = None
        if self._supports_interactive_buttons(request.channel):
            buttons = []
            for index, skill in enumerate(items, start=1):
                if not skill.removable:
                    continue
                buttons.append(
                    [
                        {
                            "text": f"删除 {index}",
                            "callback_data": f"skills:{request.request_id}:remove:{index}",
                        }
                    ]
                )
            buttons.extend(self._navigation_buttons(request, page, total_pages))
            buttons.append(
                [
                    {"text": "返回", "callback_data": f"skills:{request.request_id}:root"},
                    {"text": "关闭", "callback_data": f"skills:{request.request_id}:close"},
                ]
            )
        return "已安装技能", "\n".join(text_lines), buttons

    def _build_market_view(
            self,
            request: PendingSkillsInteraction,
            force_market_refresh: bool = False,
    ) -> Tuple[str, str, Optional[List[List[dict]]]]:
        """
        构建技能市场视图，仅展示尚未安装的技能。
        """
        market_skills = self._get_market_skills(
            request=request,
            force_market_refresh=force_market_refresh,
        )
        items, page, total_pages = self._page_items(
            items=market_skills,
            page=request.market_page,
            page_size=self._page_size(request.channel),
        )
        request.market_page = page

        text_lines = [f"第 {page + 1}/{total_pages} 页，共 {len(market_skills)} 个可安装技能"]
        if request.market_query:
            text_lines.append(f"当前搜索：{request.market_query}")
        if request.awaiting_input == "market-search":
            text_lines.extend(
                [
                    "",
                    "搜索输入中：直接回复关键词即可筛选市场技能，回复 `取消` 结束输入。",
                ]
            )
        if not items:
            text_lines.append("")
            if request.market_query:
                text_lines.append("当前搜索没有匹配的市场技能")
            else:
                text_lines.append("当前没有可安装的市场技能")
        else:
            for index, skill in enumerate(items, start=1):
                text_lines.extend(
                    [
                        "",
                        f"{index}. {skill.id} ({skill.source_label})",
                        self._truncate(skill.description),
                    ]
                )
                if skill.source_type == "registry":
                    text_lines.append("社区源，安装前请自行甄别安全性")

        if any(skill.source_type == "registry" for skill in items):
            text_lines.extend(
                [
                    "",
                    "提示：ClawHub 属于社区注册表，技能质量与安全性需要自行甄别。",
                ]
            )

        text_lines.extend(
            [
                "",
                "回复 `搜索 <关键词>` 筛选技能，回复 `清除搜索` 恢复全量列表，回复 `安装 <序号>` 安装技能，回复 `刷新` 重新拉取市场，回复 `n/p` 翻页，回复 `返回` 回到菜单，回复 `退出` 结束交互",
            ]
        )

        buttons = None
        if self._supports_interactive_buttons(request.channel):
            buttons = []
            search_row = []
            if request.market_query:
                search_row.append(
                    {
                        "text": "清除搜索",
                        "callback_data": f"skills:{request.request_id}:clear-search",
                    }
                )
            else:
                search_row.append(
                    {
                        "text": "搜索",
                        "callback_data": f"skills:{request.request_id}:search",
                    }
                )
            if search_row:
                buttons.append(search_row)
            for index, _skill in enumerate(items, start=1):
                buttons.append(
                    [
                        {
                            "text": f"安装 {index}",
                            "callback_data": f"skills:{request.request_id}:install:{index}",
                        }
                    ]
                )
            buttons.extend(self._navigation_buttons(request, page, total_pages))
            buttons.append(
                [
                    {"text": "刷新", "callback_data": f"skills:{request.request_id}:refresh"},
                    {"text": "返回", "callback_data": f"skills:{request.request_id}:root"},
                    {"text": "关闭", "callback_data": f"skills:{request.request_id}:close"},
                ]
            )
        return "技能市场", "\n".join(text_lines), buttons

    def _build_sources_view(
            self,
            request: PendingSkillsInteraction,
    ) -> Tuple[str, str, Optional[List[List[dict]]]]:
        """
        构建技能源管理视图，提供自定义 GitHub 源的增删入口。
        """
        sources = self.skillhelper.list_market_source_entries()
        custom_count = len([source for source in sources if not source.builtin])
        text_lines = [
            f"当前技能源：{len(sources)}",
            f"自定义技能源：{custom_count}",
        ]
        if request.awaiting_input == "source-add":
            text_lines.extend(
                [
                    "",
                    "添加输入中：直接回复 GitHub 仓库地址即可。",
                    "支持 owner/repo、https://github.com/owner/repo，或 /tree/<branch>/<skills_path> 形式。",
                    "回复 `取消` 结束输入。",
                ]
            )

        if not sources:
            text_lines.extend(["", "当前没有可用技能源"])
        else:
            for index, market_source in enumerate(sources, start=1):
                state = "自定义可删" if market_source.removable else "内置默认"
                text_lines.extend(
                    [
                        "",
                        f"{index}. {market_source.label}（{state}）",
                        self._truncate(market_source.source, limit=200),
                    ]
                )

        text_lines.extend(
            [
                "",
                "回复 `添加源 <GitHub仓库地址>` 添加自定义源，回复 `删除源 <序号>` 删除自定义源，回复 `返回` 回到菜单，回复 `退出` 结束交互",
            ]
        )

        buttons = None
        if self._supports_interactive_buttons(request.channel):
            buttons = [
                [
                    {
                        "text": "添加自定义源",
                        "callback_data": f"skills:{request.request_id}:source-add",
                    }
                ]
            ]
            for index, market_source in enumerate(sources, start=1):
                if not market_source.removable:
                    continue
                buttons.append(
                    [
                        {
                            "text": f"删除 {index}",
                            "callback_data": f"skills:{request.request_id}:source-remove:{index}",
                        }
                    ]
                )
            buttons.append(
                [
                    {"text": "返回", "callback_data": f"skills:{request.request_id}:root"},
                    {"text": "关闭", "callback_data": f"skills:{request.request_id}:close"},
                ]
            )
        return "技能源管理", "\n".join(text_lines), buttons

    @staticmethod
    def _truncate(text: str, limit: int = 140) -> str:
        """
        对技能描述做轻量截断，避免消息过长。
        """
        if len(text) <= limit:
            return text
        return text[: limit - 3] + "..."

    @staticmethod
    def _page_items(
            items: List[SkillInfo],
            page: int,
            page_size: int,
    ) -> Tuple[List[SkillInfo], int, int]:
        """
        返回当前页的数据，并把页码钳制到有效范围内。
        """
        return page_items(items=items, page=page, page_size=page_size)

    def _page_size(self, channel: Optional[MessageChannel]) -> int:
        """
        按渠道能力选择分页大小，按钮渠道单页更短，便于直接操作。
        """
        return (
            self._button_page_size
            if self._supports_interactive_buttons(channel)
            else self._text_page_size
        )

    @staticmethod
    def _supports_interactive_buttons(channel: Optional[MessageChannel]) -> bool:
        """
        判断当前渠道是否同时支持按钮展示和回调。
        """
        return supports_interaction_buttons(channel)

    @staticmethod
    def _navigation_buttons(
            request: PendingSkillsInteraction,
            page: int,
            total_pages: int,
    ) -> List[List[dict]]:
        """
        为分页视图生成上一页和下一页按钮。
        """
        return build_navigation_buttons(
            prefix="skills",
            request=request,
            page=page,
            total_pages=total_pages,
        )

    def _update_or_post_message(
            self,
            channel: MessageChannel,
            source: Optional[str],
            userid: Union[str, int],
            username: Optional[str],
            title: str,
            text: str,
            buttons: Optional[List[List[dict]]] = None,
            original_message_id: Optional[Union[str, int]] = None,
            original_chat_id: Optional[str] = None,
    ) -> None:
        """
        优先编辑原消息，编辑失败时再回退为发送新消息。
        """
        update_or_post_message(
            chain=self,
            channel=channel,
            source=source,
            userid=userid,
            username=username,
            title=title,
            text=text,
            buttons=buttons,
            original_message_id=original_message_id,
            original_chat_id=original_chat_id,
        )

    @staticmethod
    def _build_usage_hint(view: str) -> str:
        """
        根据当前视图返回可执行的文本命令提示。
        """
        if view == "market":
            return "请输入 搜索 <关键词>、清除搜索、安装 <序号>、刷新、n、p、返回 或 退出"
        if view == "installed":
            return "请输入 删除 <序号>、n、p、返回 或 退出"
        if view == "sources":
            return "请输入 添加源 <GitHub仓库地址>、删除源 <序号>、返回 或 退出"
        return "请输入 1、2、3、搜索 <关键词>、刷新 或 退出"

    def _get_market_skills(
            self,
            request: PendingSkillsInteraction,
            force_market_refresh: bool = False,
    ) -> List[SkillInfo]:
        """
        获取当前 /skills 会话可见的市场技能，并应用搜索词过滤。
        """
        skills = [
            skill
            for skill in self.skillhelper.list_market_skills(force=force_market_refresh)
            if not skill.installed
        ]
        if not request.market_query:
            return skills
        return self.skillhelper.filter_market_skills(
            skills=skills,
            query=request.market_query,
        )

    @staticmethod
    def _extract_market_search_query(text: str) -> str:
        """
        从文本命令中提取市场搜索词，兼容“搜索/查找/查”前缀。
        """
        normalized = (text or "").strip()
        if not normalized:
            return ""
        match = re.match(r"^(?:搜索|查找|查)\s+(.+)$", normalized)
        return match.group(1).strip() if match else ""

    @staticmethod
    def _extract_market_source_input(text: str) -> str:
        """
        从文本命令中提取自定义技能源地址。
        """
        normalized = (text or "").strip()
        if not normalized:
            return ""
        match = re.match(
            r"^(?:添加源|新增源|添加仓库|新增仓库|add source)\s+(.+)$",
            normalized,
            re.IGNORECASE,
        )
        return match.group(1).strip() if match else ""

    @staticmethod
    def _apply_market_search(
            request: PendingSkillsInteraction,
            query: str,
    ) -> None:
        """
        将会话切到市场搜索结果视图，并重置分页状态。
        """
        request.view = "market"
        request.market_query = (query or "").strip()
        request.market_page = 0
        request.awaiting_input = None

    @staticmethod
    def _clear_market_search(request: PendingSkillsInteraction) -> None:
        """
        清除当前市场搜索状态，恢复全量市场列表。
        """
        request.market_query = ""
        request.market_page = 0
        request.awaiting_input = None
