import base64
import re
from datetime import datetime
from typing import Any, Callable, Optional, Tuple, Union, Dict
from urllib.parse import urljoin

from app.application.site.sites import SitesHelper  # pylint: disable=import-error,no-name-in-module
from lxml import etree

from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin
from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager
from app.application.chain.data import SitePortProxy as SiteOper
from app.application.configuration import get_configured_system_config
from app.adapters.network.browser import PlaywrightHelper
from app.adapters.network.cloudflare import under_challenge
from app.application.security.cookie import CookieHelper
from app.adapters.external.cookiecloud import CookieCloudHelper
from app.application.messaging.site import SiteInteractionHandler
from app.application.rss import RssHelper
from app.runtime.log import logger
from app.schemas.notification import NotificationChannel
from app.schemas.message import Message
from app.schemas.site import SiteUserData
from app.schemas.types import EventType, MessageType
from app.adapters.network.http import RequestUtils
from app.domain.site import SiteUtils
from app.domain import site as site_rules
from app.foundation import size as size_tools
from app.foundation import url as url_tools
from app.foundation.dom import DomUtils

Site = Any


class SiteChain(InteractionChainMixin, ChainBase):
    """
    站点管理处理链
    """

    # 交互处理器类注入，供 InteractionChainMixin 的 parse_callback 委托
    _interaction_handler_type = SiteInteractionHandler

    def __init__(self):
        """初始化站点管理处理链及特殊站点测试器"""
        super().__init__()

        # 特殊站点登录验证
        self.special_site_test = {
            "zhuque.in": self.__zhuque_test,
            "m-team.io": self.__mteam_test,
            "m-team.cc": self.__mteam_test,
            "ptlsp.com": self.__indexphp_test,
            "1ptba.com": self.__indexphp_test,
            "star-space.net": self.__indexphp_test,
            "yemapt.org": self.__yema_test,
            "hddolby.com": self.__hddolby_test,
            "rousi.pro": self.__rousi_test,
            "sunnypt.top": self.__sunnypt_test,
        }

    def refresh_userdata(self, site: dict = None) -> Optional[SiteUserData]:
        """
        刷新站点的用户数据
        :param site:  站点
        :return: 用户数据
        """
        userdata: SiteUserData = self.run_module("refresh_userdata", site=site)
        if userdata:
            SiteOper().update_userdata(domain=site_rules.extract_domain(site.get("domain")),
                                       name=site.get("name"),
                                       payload=userdata.model_dump())
            # 发送事件
            eventmanager.send_event(EventType.SiteRefreshed, {
                "site_id": site.get("id")
            })
            self._post_site_messages(site=site, userdata=userdata)
            # 低分享率警告
            if userdata.ratio and float(userdata.ratio) < 1 and not bool(
                    re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)):
                self.post_message(Message(
                    mtype=MessageType.SiteMessage,
                    title=f"【站点分享率低预警】",
                    text=f"站点 {site.get('name')} 分享率 {userdata.ratio}，请注意！"
                ))
        return userdata

    def _post_site_messages(self, site: dict, userdata: SiteUserData) -> None:
        """
        发送站点未读消息，并按解析器提供的来源标识做持久化去重。

        :param site: 站点索引配置
        :param userdata: 本次刷新的站点用户数据
        """
        if not userdata.message_unread:
            return
        if not userdata.message_unread_contents:
            self.post_message(Message(
                mtype=MessageType.SiteMessage,
                title=f"站点 {site.get('name')} 收到 "
                      f"{userdata.message_unread} 条新消息，请登陆查看",
                link=site.get("url")
            ))
            return
        for message in userdata.message_unread_contents:
            head, date, content, *metadata = message
            message_source = metadata[0] if metadata else None
            if message_source and self.messageoper.exists_by_source(message_source):
                continue
            msg_title = f"【站点 {site.get('name')} 消息】"
            msg_text = f"时间：{date}\n标题：{head}\n内容：\n{content}"
            self.post_message(Message(
                source=message_source,
                mtype=MessageType.SiteMessage,
                title=msg_title,
                text=msg_text,
                link=site.get("url")
            ))

    def refresh_userdatas(
            self,
            progress_callback: Optional[Callable[..., None]] = None,
    ) -> Optional[Dict[str, SiteUserData]]:
        """
        刷新所有站点的用户数据

        :param progress_callback: 定时服务进度更新回调
        """
        any_site_updated = False
        result = {}
        sites = [site for site in SitesHelper().get_indexers() if site.get("is_active")]
        total_num = len(sites)
        if progress_callback:
            progress_callback(
                value=0,
                text=f"开始刷新站点数据，共 {total_num} 个站点 ...",
                data={"total": total_num, "finished": 0},
            )
        for index, site in enumerate(sites, start=1):
            if global_vars.is_system_stopped:
                return None
            if progress_callback:
                progress_callback(
                    value=(index - 1) / total_num * 100 if total_num else 100,
                    text=f"正在刷新站点数据（{index}/{total_num}）{site.get('name')} ...",
                    data={
                        "total": total_num,
                        "finished": index - 1,
                        "current": site.get("id"),
                    },
                )
            userdata = self.refresh_userdata(site)
            if userdata:
                any_site_updated = True
                result[site.get("name")] = userdata
            if progress_callback:
                progress_callback(
                    value=index / total_num * 100 if total_num else 100,
                    text=f"站点数据（{index}/{total_num}）刷新完成",
                    data={"total": total_num, "finished": index},
                )
        if any_site_updated:
            eventmanager.send_event(EventType.SiteRefreshed, {
                "site_id": "*"
            })
        if progress_callback:
            progress_callback(value=100, text="站点数据刷新完成")

        return result

    def is_special_site(self, domain: str) -> bool:
        """
        判断是否特殊站点
        """
        return domain in self.special_site_test

    def __zhuque_test(self, site: Site) -> Tuple[bool, str]:
        """
        判断站点是否已经登陆：zhuique
        """
        # 获取token
        token = None
        user_agent = site.ua or self.runtime_config.user_agent
        res = RequestUtils(
            ua=user_agent,
            cookies=site.cookie,
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15
        ).get_res(url=site.url)
        if res is None:
            return False, "无法打开网站！"
        if res.status_code == 200:
            csrf_token = re.search(r'<meta name="x-csrf-token" content="(.+?)">', res.text)
            if csrf_token:
                token = csrf_token.group(1)
        else:
            return False, f"错误：{res.status_code} {res.reason}"
        if not token:
            return False, "无法获取Token"
        # 调用查询用户信息接口
        user_res = RequestUtils(
            headers={
                'X-CSRF-TOKEN': token,
                "Content-Type": "application/json; charset=utf-8",
                "User-Agent": f"{user_agent}"
            },
            cookies=site.cookie,
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15
        ).get_res(url=f"{site.url}api/user/getInfo")
        if user_res is None:
            return False, "无法打开网站！"
        if user_res.status_code == 200:
            user_info = user_res.json()
            if user_info and user_info.get("data"):
                return True, "连接成功"
            return False, "Cookie已失效"
        else:
            return False, f"错误：{user_res.status_code} {user_res.reason}"

    def __mteam_test(self, site: Site) -> Tuple[bool, str]:
        """
        判断站点是否已经登陆：m-team
        """
        user_agent = site.ua or self.runtime_config.user_agent
        domain = site_rules.extract_domain(site.url)
        url = f"https://api.{domain}/api/member/profile"
        headers = {
            "User-Agent": user_agent,
            "Accept": "application/json, text/plain, */*",
            "x-api-key": site.apikey,
        }
        res = RequestUtils(
            headers=headers,
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15
        ).post_res(url=url)
        if res is None:
            return False, "无法打开网站！"
        if res.status_code == 200:
            user_info = res.json() or {}
            if user_info.get("data"):
                return True, "连接成功"
            return False, user_info.get("message", "鉴权已过期或无效")
        else:
            return False, f"错误：{res.status_code} {res.reason}"

    def __sunnypt_test(self, site: Site) -> Tuple[bool, str]:
        """
        通过 profile 接口测试 SunnyPT API Key 和下载权限

        :param site: SunnyPT 站点配置
        :return: 是否可用及状态信息
        """
        indexer = SitesHelper().get_indexer(site.domain) or {}
        api_url = str(
            indexer.get("api_url") or "https://api.sunnypt.top/api/v1/mp"
        ).rstrip("/")
        res = RequestUtils(
            headers={
                "Accept": "application/json",
                "User-Agent": site.ua or self.runtime_config.user_agent,
                "X-API-Key": site.apikey,
            },
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15,
        ).get_res(url=f"{api_url}/profile")
        if res is None:
            return False, "无法连接 SunnyPT API 服务"
        if res.status_code != 200:
            return False, f"错误：{res.status_code} {res.reason}"
        try:
            payload = res.json() or {}
        except (TypeError, ValueError):
            return False, "SunnyPT API 响应不是有效 JSON"
        if str(payload.get("code")) != "0" or not isinstance(payload.get("data"), dict):
            return False, payload.get("msg") or "API Key 已过期或无效"
        if payload["data"].get("download_allowed") is False:
            return False, "当前账号没有下载权限"
        return True, "连接成功"

    def __yema_test(self, site: Site) -> Tuple[bool, str]:
        """
        判断站点是否已经登陆：yemapt
        """
        user_agent = site.ua or self.runtime_config.user_agent
        url = f"{site.url}api/consumer/fetchSelfDetail"
        headers = {
            "User-Agent": user_agent,
            "Content-Type": "application/json",
            "Accept": "application/json, text/plain, */*",
        }
        res = RequestUtils(
            headers=headers,
            cookies=site.cookie,
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15
        ).get_res(url=url)
        if res is None:
            return False, "无法打开网站！"
        if res.status_code == 200:
            user_info = res.json()
            if user_info and user_info.get("success"):
                return True, "连接成功"
            return False, "Cookie已过期"
        else:
            return False, f"错误：{res.status_code} {res.reason}"

    def __indexphp_test(self, site: Site) -> Tuple[bool, str]:
        """
        判断站点是否已经登陆：ptlsp/1ptba
        """
        site.url = f"{site.url}index.php"
        return self.__test(site)

    def __hddolby_test(self, site: Site) -> Tuple[bool, str]:
        """
        判断站点是否已经登陆：hddolby
        """
        url = f"{site.url}api/v1/user/data"
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json, text/plain, */*",
            "x-api-key": site.apikey,
        }
        res = RequestUtils(
            headers=headers,
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15
        ).get_res(url=url)
        if res is None:
            return False, "无法打开网站！"
        if res.status_code == 200:
            user_info = res.json()
            if user_info and user_info.get("status") == 0:
                return True, "连接成功"
            return False, "APIKEY已过期"
        else:
            return False, f"错误：{res.status_code} {res.reason}"

    def __rousi_test(self, site: Site) -> Tuple[bool, str]:
        """
        判断站点是否已经登陆：rousi
        """
        url = f"https://{site_rules.extract_domain(site.url)}/api/v1/profile"
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": f"Bearer {site.apikey}",
        }
        res = RequestUtils(
            headers=headers,
            proxies=self.runtime_config.proxy if site.proxy else None,
            timeout=site.timeout or 15
        ).get_res(url=url)
        if res is None:
            return False, "无法打开网站！"
        if res.status_code == 200:
            user_info = res.json()
            if user_info and user_info.get("code") == 0:
                return True, "连接成功"
            return False, "APIKEY已过期"
        else:
            return False, f"错误：{res.status_code} {res.reason}"

    @staticmethod
    def __parse_favicon(url: str, cookie: str, ua: str) -> Tuple[str, Optional[str]]:
        """
        解析站点favicon,返回base64 fav图标
        :param url: 站点地址
        :param cookie: Cookie
        :param ua: User-Agent
        :return:
        """
        favicon_url = urljoin(url, "favicon.ico")
        res = RequestUtils(cookies=cookie, timeout=30, ua=ua).get_res(url=url)
        if res:
            html_text = res.text
        else:
            logger.error(f"获取站点页面失败：{url}")
            return favicon_url, None
        html = etree.HTML(html_text)
        try:
            if DomUtils.has_child_elements(html):
                fav_link = html.xpath('//head/link[contains(@rel, "icon")]/@href')
                if fav_link:
                    favicon_url = urljoin(url, fav_link[0])

            res = RequestUtils(cookies=cookie, timeout=15, ua=ua).get_res(url=favicon_url)
            if res:
                return favicon_url, base64.b64encode(res.content).decode()
            else:
                logger.error(f"获取站点图标失败：{favicon_url}")
        finally:
            if html is not None:
                del html
        return favicon_url, None

    def sync_cookies(
            self,
            manual: bool = False,
            progress_callback: Optional[Callable[..., None]] = None,
    ) -> Tuple[bool, str]:
        """
        通过CookieCloud同步站点Cookie

        :param manual: 是否手动同步
        :param progress_callback: 定时服务进度更新回调
        """

        logger.info("开始同步CookieCloud站点 ...")
        if progress_callback:
            progress_callback(value=0, text="开始下载 CookieCloud 数据 ...")
        cookies, msg = CookieCloudHelper().download()
        if not cookies:
            logger.error(f"CookieCloud同步失败：{msg}")
            if progress_callback:
                progress_callback(value=100, text=f"CookieCloud同步失败：{msg}")
            if manual:
                self.messagehelper.put(msg, title="CookieCloud同步失败", role="system")
            return False, msg
        siteshelper = SitesHelper()
        siteoper = SiteOper()
        rsshelper = RssHelper()
        total_num = len(cookies)
        update_count = add_count = fail_count = 0
        for index, (domain, cookie) in enumerate(cookies.items(), start=1):
            # 检查系统是否停止
            if global_vars.is_system_stopped:
                logger.info("系统正在停止，中断CookieCloud同步")
                return False, "系统正在停止，同步被中断"
            if progress_callback:
                progress_callback(
                    value=(index - 1) / total_num * 100 if total_num else 100,
                    text=f"正在同步 CookieCloud 站点（{index}/{total_num}）{domain} ...",
                    data={
                        "total": total_num,
                        "finished": index - 1,
                        "current": domain,
                    },
                )

            indexer = siteshelper.get_indexer(domain)
            site_info = siteoper.get_by_domain(domain)
            updated, added, failed, should_finalize = self._sync_cookiecloud_domain(
                domain=domain,
                cookie=cookie,
                indexer=indexer,
                site_info=site_info,
                siteoper=siteoper,
                rsshelper=rsshelper,
            )
            update_count += updated
            add_count += added
            fail_count += failed
            if not should_finalize:
                continue

            # 通知站点更新
            if indexer:
                eventmanager.send_event(EventType.SiteUpdated, {
                    "domain": domain,
                })
            if progress_callback:
                progress_callback(
                    value=index / total_num * 100 if total_num else 100,
                    text=f"CookieCloud 站点（{index}/{total_num}）同步完成",
                    data={"total": total_num, "finished": index},
                )
        # 处理完成
        ret_msg = f"更新了{update_count}个站点，新增了{add_count}个站点"
        if fail_count > 0:
            ret_msg += f"，{fail_count}个站点添加失败，下次同步时将重试，也可以手动添加"
        if manual:
            self.messagehelper.put(ret_msg, title="CookieCloud同步成功", role="system")
        logger.info(f"CookieCloud同步成功：{ret_msg}")
        if progress_callback:
            progress_callback(value=100, text=f"CookieCloud同步成功：{ret_msg}")
        return True, ret_msg

    def _sync_cookiecloud_domain(
            self,
            domain: str,
            cookie: str,
            indexer: Optional[dict],
            site_info: Any,
            siteoper: SiteOper,
            rsshelper: RssHelper,
    ) -> Tuple[int, int, int, bool]:
        """处理单个域名，并返回计数与是否继续发送更新事件。"""
        if site_info and site_info.is_active:
            status, _ = self.test(domain)
            if status:
                logger.info(f"站点【{site_info.name}】连通性正常，不同步CookieCloud数据")
                if not site_info.public and not site_info.rss:
                    rss_url, errmsg = rsshelper.get_rss_link(
                        url=site_info.url,
                        cookie=cookie,
                        ua=site_info.ua or self.runtime_config.user_agent,
                        proxy=bool(site_info.proxy),
                        timeout=site_info.timeout or 15,
                    )
                    if rss_url:
                        siteoper.update_rss(domain=domain, rss=rss_url)
                    else:
                        logger.warning(errmsg)
                return 0, 0, 0, False
            logger.info(f"更新站点 {domain} Cookie ...")
            siteoper.update_cookie(domain=domain, cookies=cookie)
            return 1, 0, 0, True
        if not indexer:
            return 0, 0, 0, True
        if self._cookiecloud_blacklisted(domain):
            logger.warning(f"站点 {domain} 已在黑名单中，不添加站点")
            return 0, 0, 0, False
        domain_url = self._cookiecloud_indexer_domain(indexer, domain)
        proxy, response = self._cookiecloud_connect(domain_url, cookie, indexer)
        if response is None:
            return 0, 0, 1, False
        if response.status_code not in [200, 500, 403]:
            logger.warning(f"站点 {indexer.get('name')} 连接状态码：{response.status_code}，无法添加站点")
            return 0, 0, 1, False
        if not indexer.get("public") and not SiteUtils.is_logged_in(response.text):
            logger.warning(f"站点 {indexer.get('name')} 登录失败，无法添加站点")
            return 0, 0, 1, False
        rss_url = None
        if not indexer.get("public") and domain_url:
            rss_url, errmsg = rsshelper.get_rss_link(
                url=domain_url, cookie=cookie, ua=self.runtime_config.user_agent, proxy=proxy
            )
            if errmsg:
                logger.warning(errmsg)
        siteoper.add(
            name=indexer.get("name"), url=domain_url, domain=domain, cookie=cookie,
            rss=rss_url, proxy=1 if proxy else 0, public=1 if indexer.get("public") else 0,
        )
        return 0, 1, 0, True

    def _cookiecloud_blacklisted(self, domain: str) -> bool:
        """判断域名是否命中 CookieCloud 黑名单。"""
        blacklist = self.runtime_config.cookiecloud_blacklist
        return bool(blacklist) and any(
            site_rules.extract_domain(domain) == site_rules.extract_domain(item)
            for item in str(blacklist).split(",")
        )

    @staticmethod
    def _cookiecloud_indexer_domain(indexer: dict, sub_domain: str) -> str:
        """根据索引器主域名和扩展域名解析实际访问地址。"""
        if site_rules.extract_domain(indexer.get("domain")) == sub_domain:
            return indexer.get("domain")
        for ext_domain in indexer.get("ext_domains", []):
            if site_rules.extract_domain(ext_domain) == sub_domain:
                return ext_domain
        return sub_domain

    def _cookiecloud_connect(self, domain_url: str, cookie: str, indexer: dict) -> Tuple[bool, Any]:
        """连接新站点，必要时通过已配置代理重试。"""
        response = RequestUtils(cookies=cookie, ua=self.runtime_config.user_agent).get_res(url=domain_url)
        if response is not None or not self.runtime_config.proxy_host:
            return False, response
        logger.info(f"站点 {indexer.get('name')} 初次连接失败，尝试通过代理重试...")
        response = RequestUtils(
            cookies=cookie, ua=self.runtime_config.user_agent, proxies=self.runtime_config.proxy
        ).get_res(url=domain_url)
        return True, response

    @eventmanager.register(EventType.SiteUpdated)
    def cache_site_icon(self, event: Event):
        """
        缓存站点图标
        """
        if not event:
            return
        event_data = event.event_data or {}
        # 主域名
        domain = event_data.get("domain")
        if not domain:
            return
        if str(domain).startswith("http"):
            domain = site_rules.extract_domain(domain)
        # 站点信息
        siteoper = SiteOper()
        siteshelper = SitesHelper()
        siteinfo = siteoper.get_by_domain(domain)
        if not siteinfo:
            logger.warn(f"未维护站点 {domain} 信息！")
            return
        # Cookie
        cookie = siteinfo.cookie
        # 索引器
        indexer = siteshelper.get_indexer(domain)
        if not indexer:
            logger.warn(f"站点 {domain} 索引器不存在！")
            return
        # 查询站点图标
        logger.info(f"开始缓存站点 {indexer.get('name')} 图标 ...")
        icon_url, icon_base64 = self.__parse_favicon(url=indexer.get("domain"),
                                                     cookie=cookie,
                                                     ua=self.runtime_config.user_agent)
        if icon_url:
            siteoper.update_icon(name=indexer.get("name"),
                                 domain=domain,
                                 icon_url=icon_url,
                                 icon_base64=icon_base64)
            logger.info(f"缓存站点 {indexer.get('name')} 图标成功")
        else:
            logger.warn(f"缓存站点 {indexer.get('name')} 图标失败")

    @eventmanager.register(EventType.SiteUpdated)
    def clear_site_data(self, event: Event):
        """
        清理站点数据
        """
        if not event:
            return
        event_data = event.event_data or {}
        # 主域名
        domain = event_data.get("domain")
        if not domain:
            return
        # 获取主域名中间那段
        domain_host = url_tools.host_label(domain)
        # 查询以"site.domain_host"开头的配置项，并清除
        # SystemConfigService 未提供 all()，无参 get() 经仓库返回全部配置字典
        systemconfig = get_configured_system_config()
        site_keys = systemconfig.get().keys()
        for key in site_keys:
            if key.startswith(f"site.{domain_host}"):
                logger.info(f"清理站点配置：{key}")
                systemconfig.delete(key)

    @eventmanager.register(EventType.SiteUpdated)
    def cache_site_userdata(self, event: Event):
        """
        缓存站点用户数据
        """
        if not event:
            return
        event_data = event.event_data or {}
        # 主域名
        domain = event_data.get("domain")
        if not domain:
            return
        if str(domain).startswith("http"):
            domain = site_rules.extract_domain(domain)
        indexer = SitesHelper().get_indexer(domain)
        if not indexer:
            return
        # 刷新站点用户数据
        self.refresh_userdata(site=indexer) or {}

    def test(self, url: str) -> Tuple[bool, str]:
        """
        测试站点是否可用
        :param url: 站点域名
        :return: (是否可用, 错误信息)
        """
        # 检查域名是否可用
        domain = site_rules.extract_domain(url)
        siteoper = SiteOper()
        site_info = siteoper.get_by_domain(domain)
        if not site_info:
            return False, f"站点【{url}】不存在"

        # 模拟登录
        try:
            # 开始记时
            start_time = datetime.now()
            # 特殊站点测试
            if self.special_site_test.get(domain):
                state, message = self.special_site_test[domain](site_info)
            else:
                # 通用站点测试
                state, message = self.__test(site_info)
            # 统计
            seconds = (datetime.now() - start_time).seconds
            if state:
                siteoper.success(domain=domain, seconds=seconds)
            else:
                siteoper.fail(domain)
            return state, message
        except Exception as e:
            return False, f"{str(e)}！"

    def __test(self, site_info: Site) -> Tuple[bool, str]:
        """
        通用站点测试
        """
        site_url = site_info.url
        site_cookie = site_info.cookie
        ua = site_info.ua or self.runtime_config.user_agent
        render = site_info.render
        public = site_info.public
        proxies = self.runtime_config.proxy if site_info.proxy else None
        proxy_server = self.runtime_config.proxy_server if site_info.proxy else None
        timeout = site_info.timeout or 60

        # 访问链接
        if render:
            page_source = PlaywrightHelper().get_page_source(url=site_url,
                                                             cookies=site_cookie,
                                                             ua=ua,
                                                             proxies=proxy_server,
                                                             timeout=timeout)
            if not public and not SiteUtils.is_logged_in(page_source):
                if under_challenge(page_source):
                    return False, f"无法通过Cloudflare！"
                return False, f"仿真登录失败，Cookie已失效！"
        else:
            res = RequestUtils(cookies=site_cookie,
                               ua=ua,
                               proxies=proxies
                               ).get_res(url=site_url)
            # 判断登录状态
            if res and res.status_code in [200, 500, 403]:
                content = res.text
                if not public and not SiteUtils.is_logged_in(content):
                    if under_challenge(content):
                        msg = "站点被Cloudflare防护，请打开站点浏览器仿真"
                    elif res.status_code == 200:
                        msg = "Cookie已失效"
                    else:
                        msg = f"错误：{res.status_code} {res.reason}"
                    return False, f"{msg}！"
                elif public and res.status_code != 200:
                    return False, f"错误：{res.status_code} {res.reason}！"
            elif res is not None:
                return False, f"错误：{res.status_code} {res.reason}！"
            else:
                return False, f"无法打开网站！"
        return True, "连接成功"

    def _interaction_handler(self) -> "SiteInteractionHandler":
        """构造 /sites 交互处理器，Cookie 更新动作由本链提供。"""
        return SiteInteractionHandler(
            messenger=self,
            cookie_updater=self.update_cookie,
            repository=SiteOper(),
        )

    def remote_disable(self, arg_str: str, channel: NotificationChannel,
                       userid: Union[str, int] = None, source: Optional[str] = None):
        """
        禁用站点
        """
        if not arg_str:
            return
        arg_str = str(arg_str).strip()
        if not arg_str.isdigit():
            return
        site_id = int(arg_str)
        siteoper = SiteOper()
        site = siteoper.get(site_id)
        if not site:
            self.post_message(Message(
                channel=channel,
                title=f"站点编号 {site_id} 不存在！",
                userid=userid,
                save_history=False))
            return
        # 禁用站点
        siteoper.update(site_id, {
            "is_active": False
        })
        # 重新发送消息
        self.remote_list(channel=channel, userid=userid, source=source)

    def remote_enable(self, arg_str: str, channel: NotificationChannel,
                      userid: Union[str, int] = None, source: Optional[str] = None):
        """
        启用站点
        """
        if not arg_str:
            return
        arg_strs = str(arg_str).split()
        siteoper = SiteOper()
        for arg_str in arg_strs:
            arg_str = arg_str.strip()
            if not arg_str.isdigit():
                continue
            site_id = int(arg_str)
            site = siteoper.get(site_id)
            if not site:
                self.post_message(Message(
                    channel=channel,
                    title=f"站点编号 {site_id} 不存在！",
                    userid=userid,
                    save_history=False))
                return
            # 禁用站点
            siteoper.update(site_id, {
                "is_active": True
            })
        # 重新发送消息
        self.remote_list(channel=channel, userid=userid, source=source)

    def update_cookie(self, site_info: Site,
                      username: str, password: str, two_step_code: Optional[str] = None) -> Tuple[bool, str]:
        """
        根据用户名密码更新站点Cookie
        :param site_info: 站点信息
        :param username: 用户名
        :param password: 密码
        :param two_step_code: 二步验证码或密钥
        :return: (是否成功, 错误信息)
        """
        # 更新站点Cookie
        result = CookieHelper().get_site_cookie_ua(
            url=site_info.url,
            username=username,
            password=password,
            two_step_code=two_step_code,
            proxies=self.runtime_config.proxy_server if site_info.proxy else None,
            timeout=site_info.timeout or 60
        )
        if result:
            cookie, ua, msg = result
            if not cookie:
                return False, msg
            SiteOper().update(site_info.id, {
                "cookie": cookie,
                "ua": ua
            })
            return True, msg
        return False, "未知错误"

    def remote_cookie(self, arg_str: str, channel: NotificationChannel,
                      userid: Union[str, int] = None, source: Optional[str] = None):
        """
        使用用户名密码更新站点Cookie
        """
        err_title = "请输入正确的命令格式：/site_cookie [id] [username] [password] [2fa_code/secret]，" \
                    "[id]为站点编号，[uername]为站点用户名，[password]为站点密码，[2fa_code/secret]为站点二步验证码或密钥"
        if not arg_str:
            self.post_message(Message(
                channel=channel,
                source=source,
                title=err_title,
                userid=userid,
                save_history=False))
            return
        arg_str = str(arg_str).strip()
        args = arg_str.split()
        # 二步验证码
        two_step_code = None
        if len(args) == 4:
            two_step_code = args[3]
        elif len(args) != 3:
            self.post_message(Message(
                channel=channel,
                source=source,
                title=err_title,
                userid=userid,
                save_history=False))
            return
        site_id = args[0]
        if not site_id.isdigit():
            self.post_message(Message(
                channel=channel,
                source=source,
                title=err_title,
                userid=userid,
                save_history=False))
            return
        # 站点ID
        site_id = int(site_id)
        # 站点信息
        site_info = SiteOper().get(site_id)
        if not site_info:
            self.post_message(Message(
                channel=channel,
                source=source,
                title=f"站点编号 {site_id} 不存在！",
                userid=userid,
                save_history=False))
            return
        self.post_message(Message(
            channel=channel,
            source=source,
            title=f"开始更新【{site_info.name}】Cookie&UA ...",
            userid=userid,
            save_history=False))
        # 用户名
        username = args[1]
        # 密码
        password = args[2]
        # 更新Cookie
        status, msg = self.update_cookie(site_info=site_info,
                                         username=username,
                                         password=password,
                                         two_step_code=two_step_code)
        if not status:
            logger.error(msg)
            self.post_message(Message(
                channel=channel,
                source=source,
                title=f"【{site_info.name}】 Cookie&UA更新失败！",
                text=f"错误原因：{msg}",
                userid=userid,
                save_history=False))
        else:
            self.post_message(Message(
                channel=channel,
                source=source,
                title=f"【{site_info.name}】 Cookie&UA更新成功",
                userid=userid,
                save_history=False))

    def remote_refresh_userdatas(self, channel: NotificationChannel,
                                 userid: Union[str, int] = None, source: Optional[str] = None):
        """
        刷新所有站点用户数据
        """
        logger.info("收到命令，开始刷新站点数据 ...")
        self.post_message(Message(
            channel=channel,
            source=source,
            title="开始刷新站点数据 ...",
            userid=userid,
            save_history=False,
        ))
        # 刷新站点数据
        site_datas = self.refresh_userdatas()
        if site_datas:
            # 发送消息
            messages = {}
            # 总上传
            incUploads = 0
            # 总下载
            incDownloads = 0
            # 今天日期
            today_date = datetime.now().strftime("%Y-%m-%d")

            for rand, site in enumerate(site_datas.keys()):
                upload = int(site_datas[site].upload or 0)
                download = int(site_datas[site].download or 0)
                updated_date = site_datas[site].updated_day
                if updated_date and updated_date != today_date:
                    updated_date = f"（{updated_date}）"
                else:
                    updated_date = ""

                if upload > 0 or download > 0:
                    incUploads += upload
                    incDownloads += download
                    messages[upload + (rand / 1000)] = (
                            f"【{site}】{updated_date}\n"
                            + f"上传量：{size_tools.format_compact_size(upload)}\n"
                            + f"下载量：{size_tools.format_compact_size(download)}\n"
                            + "————————————"
                    )
            if incDownloads or incUploads:
                sorted_messages = [messages[key] for key in sorted(messages.keys(), reverse=True)]
                sorted_messages.insert(0, f"【汇总】\n"
                                          f"总上传：{size_tools.format_compact_size(incUploads)}\n"
                                          f"总下载：{size_tools.format_compact_size(incDownloads)}\n"
                                          f"————————————")
                self.post_message(Message(
                    channel=channel,
                    source=source,
                    title="【站点数据统计】",
                    text="\n".join(sorted_messages),
                    userid=userid,
                    save_history=False
                ))
        else:
            self.post_message(Message(
                channel=channel,
                source=source,
                title="没有刷新到任何站点数据！",
                userid=userid,
                save_history=False,
            ))
