import asyncio
import hashlib
import json
import pytz
import re
import time
import yaml
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple, Iterable, TypeVar

import jsonpatch
from fastapi import HTTPException
from apscheduler.schedulers.asyncio import AsyncIOScheduler

from app.core.cache import cached
from app.core.config import settings
from app.log import logger
from app.utils.http import AsyncRequestUtils

from .base import Constant
from .helper.clashruleparser import ClashRuleParser, RoutingRuleType, Action
from .helper.configconverter import Converter
from .helper.utilsprovider import UtilsProvider
from .models import ProxyGroup, Proxy, RuleProvider, RuleProviderData, ProxyData, HostData, VehicleType, SelectGroup, \
    ProxyGroupData, RuleItem, RuleData, Metadata, RuleProviders
from .models.api import ClashApi, SubscriptionSetting, DataUsage, SubscriptionInfo, ConfigRequest
from .models.configuration import ClashConfig
from .models.datapatch import PatchItem, DataPatch
from .models.rule import RuleType
from .models.types import DataSource, DataKey, RuleSet, ClashKey, SupportsPatch
from .state import PluginState


T = TypeVar("T")

class ClashRuleProviderService:

    def __init__(
            self, plugin_id: str,
            state: PluginState,
            scheduler: Optional[AsyncIOScheduler] = None
    ):
        self.plugin_id = plugin_id
        self.state = state
        self.scheduler = scheduler

    def save_rules(self):
        self.state.save_data(DataKey.TOP_RULES, self.state.top_rules_manager.export_rules())
        self.state.save_data(DataKey.RULESET_RULES, self.state.ruleset_rules_manager.export_rules())

    def load_rules(self):
        self.state.top_rules_manager.import_rules(self.state.get_data(DataKey.TOP_RULES) or [])
        self.state.ruleset_rules_manager.import_rules(self.state.get_data(DataKey.RULESET_RULES) or [])

    def _make_proxy_patch(self, src: Proxy, dst: Proxy):
        src_dict = src.model_dump(mode="json", by_alias=True, exclude_none=True)
        dst_dict = dst.model_dump(mode="json", by_alias=True, exclude_none=True)
        patch = jsonpatch.make_patch(src_dict, dst_dict)
        patches = self.state.proxy_patch
        patches[src.name] = PatchItem(patch=patch.to_string(), lifecycle=Constant.PATCH_LIFESPAN)
        self.state.proxy_patch = patches

    def _apply_patch(self, item: SupportsPatch[T], name: str, patch: DataPatch) -> T:
        try:
            if name in patch:
                return item.patch(patch[name].patch)
        except Exception as err:
            logger.error(f"Failed to apply patch for {name}: {repr(err)}")
        return item

    def _apply_patches(self, items: list[Any], patches: DataPatch) -> list[Any]:
        for item in items:
            item.data = self._apply_patch(item.data, item.name, patches)
            item.meta.patched = item.name in patches
        return items

    def _apply_patch_to_config(self, conf: ClashConfig) -> ClashConfig:
        conf.proxies = [self._apply_patch(proxy, proxy.name, self.state.proxy_patch) for proxy in conf.proxies]
        conf.proxy_groups = [self._apply_patch(pg, pg.name, self.state.proxy_group_patch) for pg in conf.proxy_groups]
        return conf

    def _merge_subscriptions(self, config: ClashConfig):
        subscriptions_config = self.state.config.subscriptions_config
        subscription_info = self.state.subscription_info

        for conf in subscriptions_config:
            if not subscription_info.get(conf.url).enabled:
                continue
            sub_config = self.state.get_sub_config(conf.url)
            config.merge(sub_config)

    def _filter_available_items(self, items: Iterable[Any], param: ConfigRequest) -> list[Any]:
        return [item.data for item in items if item.meta.available(param)]

    def _process_auto_rule_providers(self, config: ClashConfig):
        auto_rule_provider = {}
        ruleset_names = self.state.ruleset_names

        for r in self.state.ruleset_rules_manager.rules:
            rule = r.rule
            rule_provider_name = f'{self.state.config.ruleset_prefix}{rule.action}'
            if rule_provider_name not in auto_rule_provider:
                path_name = hashlib.sha256(rule.action.encode('utf-8')).hexdigest()[:10]
                ruleset_names[path_name] = rule_provider_name
                sub_url = (f"{self.state.config.movie_pilot_url}/api/v1/plugin/{self.plugin_id}/ruleset?"
                           f"name={path_name}&apikey={self.state.config.apikey or settings.API_TOKEN}")
                auto_rule_provider[rule_provider_name] = RuleProvider(
                    type=VehicleType.HTTP, behavior="classical", url=sub_url, path=f"./CRP/{path_name}.yaml",
                    interval=3600, format="yaml"
                )
        config.rule_providers = config.rule_providers | auto_rule_provider
        self.state.rule_provider = auto_rule_provider
        self.state.ruleset_names = ruleset_names

    def _process_rules(self, config: ClashConfig, param: ConfigRequest):
        top_rules: list[RuleType] = []
        acl4ssr_providers_map: dict[str, RuleProvider] = {}
        acl4ssr_data = self.state.acl4ssr_providers

        for r in self.state.top_rules_manager:
            if not r.meta.available(param):
                continue
            rule = r.rule
            if rule.rule_type == RoutingRuleType.RULE_SET:
                if rule.payload in acl4ssr_data:
                    acl4ssr_providers_map[rule.payload] = acl4ssr_data.get(rule.payload).data
            top_rules.append(rule)
        config.rule_providers = config.rule_providers | acl4ssr_providers_map
        config.rules = top_rules + config.rules

    def _cleanup_ruleset_names(self, config: ClashConfig):
        ruleset_names = self.state.ruleset_names
        key_to_delete = [key for key, item in ruleset_names.items() if item not in config.rule_providers]
        for key in key_to_delete:
            del ruleset_names[key]
        self.state.ruleset_names = ruleset_names

    def _check_cycles(self, config: ClashConfig):
        proxy_graph = self._build_graph(config)
        cycles = UtilsProvider.find_cycles(proxy_graph)
        if cycles:
            logger.warn("发现代理组回环：")
            for cycle in cycles:
                logger.warn(" -> ".join(cycle))

    def _make_proxy_group_patch(self, src: ProxyGroup, dst: ProxyGroup):
        src_dict = src.model_dump(mode="json", by_alias=True, exclude_none=True)
        dst_dict = dst.model_dump(mode="json", by_alias=True, exclude_none=True)
        patch = jsonpatch.make_patch(src_dict, dst_dict)

        # Flatten list patches to full replace to avoid index shift issues
        new_ops = []
        replaced_paths = set()
        list_fields = ["/proxies", "/use"]

        for op in patch.patch:
            path = op["path"]
            matched_list = next((f for f in list_fields if path == f or path.startswith(f + '/')), None)
            if matched_list:
                if matched_list not in replaced_paths:
                    field_name = matched_list.strip('/')
                    val = dst_dict.get(field_name)
                    if val is None:
                        # Removed in dst
                        new_ops.append({"op": "remove", "path": matched_list})
                    elif field_name not in src_dict:
                        # Not in src, added in dst
                        new_ops.append({"op": "add", "path": matched_list, "value": val})
                    else:
                        # In src and dst, replacing
                        new_ops.append({"op": "replace", "path": matched_list, "value": val})
                    replaced_paths.add(matched_list)
            else:
                new_ops.append(op)

        patch.patch = new_ops
        pg_patches = self.state.proxy_group_patch
        pg_patches[src.name] = PatchItem(patch=patch.to_string(), lifecycle=Constant.PATCH_LIFESPAN)
        self.state.proxy_group_patch = pg_patches

    def organize_and_save_rules(self):
        self.sync_ruleset()
        self.save_rules()

    def ruleset(self, ruleset: str) -> List[str]:
        if not ruleset.startswith(self.state.config.ruleset_prefix):
            return []
        action = ruleset[len(self.state.config.ruleset_prefix):]
        try:
            final_action = Action(action.upper())
        except ValueError:
            final_action = action
        rules = self.state.ruleset_rules_manager.filter_rules_by_action(final_action)
        return [rule.rule.condition_string() for rule in rules if rule.meta.available()]

    def sync_ruleset(self):
        outbounds = set()
        new_outbounds = set()
        manager = self.state.top_rules_manager

        manager.remove_rules_by_lambda(
            lambda r: r.rule.rule_type == RoutingRuleType.RULE_SET and
                      r.meta.source == DataSource.AUTO and
                      r.rule.payload != f"{self.state.config.ruleset_prefix}{r.rule.action}"
        )
        rules_existed = manager.filter_rules_by_condition(
            lambda r: r.meta.source == DataSource.AUTO and r.rule.rule_type == RoutingRuleType.RULE_SET
        )
        actions_existed = {r.rule.action for r in rules_existed}

        for r in self.state.ruleset_rules_manager:
            if r.meta.disabled:
                continue
            outbounds.add(r.rule.action)
            if r.rule.action not in actions_existed:
                new_outbounds.add(r.rule.action)

        manager.remove_rules_by_lambda(
            lambda r: r.rule.rule_type == RoutingRuleType.RULE_SET and
                      r.meta.source == DataSource.AUTO and
                      (r.rule.action not in outbounds)
        )

        for outbound in new_outbounds:
            clash_rule = ClashRuleParser.parse_rule_line(
                f"RULE-SET,{self.state.config.ruleset_prefix}{outbound},{outbound}")
            if clash_rule is None:
                continue
            rule = RuleItem(rule=clash_rule, meta=Metadata(source=DataSource.AUTO))
            if not manager.has_rule_item(rule):
                manager.insert_rule_at_priority(rule, 0)

    def append_top_rules(self, rules: List[str]):
        clash_rules = []
        for rule in rules:
            clash_rule = ClashRuleParser.parse_rule_line(rule)
            if clash_rule:
                clash_rules.append(RuleItem(rule=clash_rule, meta=Metadata(source=DataSource.MANUAL)))
        self.state.top_rules_manager.append_rules(clash_rules)
        self.state.save_data(DataKey.TOP_RULES, self.state.top_rules_manager.export_rules())

    def clash_outbound(self) -> list[str]:
        outbound = [pg.data.name for pg in self.state.proxy_groups]
        if self.state.clash_template:
            outbound.extend(pg.name for pg in self.state.clash_template.proxy_groups)
        if self.state.config.group_by_region or self.state.config.group_by_country:
            outbound.extend(pg.name for pg in self.proxy_groups_by_region())
        outbound.extend(pg_data.data.name for pg_data in self.state.proxy_groups_from_subs())
        outbound.extend(pg.data.name for pg in self.get_proxies())
        return outbound

    def delete_proxy(self, name: str) -> Tuple[bool, str]:
        proxies = self.state.proxies
        deleted = proxies.pop(name)
        if deleted:
            self.state.proxies = proxies
            return True, "代理删除成功"
        return False, f"代理 {name!r} 不存在"

    def delete_proxy_patch(self, name: str) -> tuple[bool, str]:
        patches = self.state.proxy_patch
        if name in patches:
            del patches.root[name]
            self.state.proxy_patch = patches
            return True, "补丁已删除"
        return False, "补丁不存在"

    def import_proxies(self, vehicle: str, payload: str) -> tuple[bool, str]:
        proxies = []
        if vehicle == 'LINK':
            links = payload.strip().splitlines()
            proxies = list(Converter().convert_v2ray(links, skip_exception=True, logger=logger).items())
        elif vehicle == 'YAML':
            try:
                imported = yaml.load(payload, Loader=yaml.SafeLoader)
                if not isinstance(imported, dict):
                    return False, "无效的输入"
            except yaml.YAMLError as err:
                logger.error(f"Failed to import rules: {repr(err)}")
                return False, 'YAML 格式错误'
            proxies = [(None, p) for p in (imported.get(DataKey.PROXIES) or [])]
        if not proxies:
            return False, "无可用节点"
        success_count = 0
        error_messages = ''
        success = True
        ps = self.state.proxies
        for item in proxies:
            try:
                proxy = Proxy.model_validate(item[1])
                meta = Metadata(source=DataSource.MANUAL)
                pd = ProxyData(data=proxy, name=proxy.name, meta=meta, raw=item[0], v2ray_link=item[0])
                if not pd.v2ray_link:
                    try:
                        pd.v2ray_link = Converter.convert_to_share_link(item[1])
                    except Exception as err:
                        logger.debug(f"Failed to convert proxy link: {repr(err)}")
                ps.add(pd)
                success_count += 1
            except Exception as err:
                success = False
                error_messages += f"{err}\n"
        message = f"导入 {success_count}/{len(proxies)} 个代理节点. \n{error_messages}"
        self.state.proxies = ps
        return success, message

    def update_proxy(self, previous_name: str, source: str, proxy: Proxy) -> tuple[bool, str]:
        if source == DataSource.MANUAL:
            proxies = self.state.proxies
            proxies.update(previous_name, ProxyData(data=proxy, name=proxy.name, meta=Metadata()))
            self.state.proxies = proxies
            return True, "代理更新成功"
        if previous_name != proxy.name:
            return False, "请勿修改代理名称"
        proxies = list(self.state.proxies_from_subs())
        src = next((g for g in proxies if g.name == previous_name), None)
        if src is None:
            return False, f"代理组 {previous_name!r} ({source}) 不存在"
        self._make_proxy_patch(src.data, proxy)
        return True, "代理更新成功"

    def update_proxy_meta(self, name: str, meta: Metadata) -> tuple[bool, str]:
        proxies = self.state.proxies
        if name not in proxies:
            return False, f"The proxy name {name} does not exist"
        proxies.set_meta(name, meta)
        self.state.proxies = proxies
        return True, ''

    def get_proxies(self, patched: bool = True) -> list[ProxyData]:
        proxies = self.state.all_proxies
        proxies = list(
            filter(lambda p: not any(keyword in p.data.name for keyword in self.state.config.filter_keywords), proxies)
        )
        if not patched:
            return proxies
        return self._apply_patches(proxies, self.state.proxy_patch)

    @cached(maxsize=1, ttl=86400, skip_empty=True)
    def _get_countries_data(self) -> List[Dict[str, str]]:
        file_path = settings.ROOT_PATH / 'app' / 'plugins' / self.plugin_id.lower() / 'countries.json'
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            logger.error(f"加载国家/地区文件错误：{e}")
            return []

    def proxy_groups_by_region(self) -> list[ProxyGroupData]:
        countries = self._get_countries_data()
        proxies = self.get_proxies()
        return self._group_by_region(
            countries, proxies, self.state.config.group_by_region, self.state.config.group_by_country
        )

    @cached(maxsize=2, ttl=86400)
    def _group_by_region(self, countries: list[dict[str, str]], proxies: list[ProxyData], group_by_continent: bool,
                         group_by_country: bool) -> list[ProxyGroupData]:
        continent_groups = {}
        country_groups = {}
        continent_map = {
            '欧洲': 'Europe', '亚洲': 'Asia', '大洋洲': 'Oceania', '非洲': 'Africa',
            '北美洲': 'NorthAmerica', '南美洲': 'SouthAmerica'
        }
        proxy_groups: list[ProxyGroup] = []
        hk = next((c for c in countries if c['abbr'] == 'HK'), {})
        tw = next((c for c in countries if c['abbr'] == 'TW'), {})

        for proxy_data in proxies:
            proxy_node = proxy_data.data
            country = ClashRuleProviderService._country_from_node(countries, proxy_node.name)
            if not country:
                continue
            if country.get("abbr") == "CN":
                if any(key in proxy_node.name for key in ("🇭🇰", "HK", "香港")):
                    country = hk
                if any(key in proxy_node.name for key in ("🇹🇼", "TW", "台湾")):
                    country = tw
            continent = continent_map.get(country["continent"])
            if continent and group_by_continent:
                continent_groups.setdefault(continent, []).append(proxy_node.name)
            if group_by_country:
                country_groups.setdefault(f"{country.get('emoji')} {country.get('chinese')}", []).append(
                    proxy_node.name)
        for continent, nodes in continent_groups.items():
            if nodes:
                proxy_groups.append(ProxyGroup(root=SelectGroup(name=continent, proxies=nodes)))

        excluded = ('中国', '香港', 'CN', 'HK', '🇨🇳', '🇭🇰')
        for continent_node in continent_groups.get('Asia', []):
            if any(x in continent_node for x in excluded):
                continue
            continent_groups.setdefault('AsiaExceptChina', []).append(continent_node)
        if continent_groups.get('AsiaExceptChina'):
            pg = SelectGroup(name="AsiaExceptChina", proxies=continent_groups['AsiaExceptChina'])
            proxy_groups.append(ProxyGroup(root=pg))
        for country, nodes in country_groups.items():
            if len(nodes):
                proxy_groups.append(ProxyGroup(root=SelectGroup(name=country, proxies=nodes)))
        country_group = list(country_groups.keys())
        if country_group:
            proxy_groups.append(ProxyGroup(root=SelectGroup(name="🏴‍☠️国家分组", proxies=country_group)))
        ret = [ProxyGroupData(name=p.name, data=p, meta=Metadata(source=DataSource.AUTO)) for p in proxy_groups]
        return ret

    @staticmethod
    def _country_from_node(countries: List[Dict[str, str]], node_name: str) -> Optional[Dict[str, str]]:
        node_name_lower = node_name.lower()
        for country in countries:
            if country.get('emoji') and country['emoji'] in node_name:
                return country
            if (
                    (country.get('chinese') and country['chinese'] in node_name) or
                    (country.get('english') and country['english'].lower() in node_name_lower)
            ):
                return country
        return None

    @staticmethod
    def _build_graph(config: ClashConfig) -> Dict[str, Any]:
        """构建代理组有向图"""
        graph = {}
        groups = config.proxy_groups
        group_names = {g.name for g in groups}
        for group in groups:
            proxies = group.proxies
            graph[group.name] = [p for p in proxies if p in group_names]
        return graph

    async def fetch_clash_data(self, endpoint: str) -> Dict:
        headers = {"Authorization": f"Bearer {self.state.config.dashboard_secret}"}
        url = f"{self.state.config.dashboard_url}/{endpoint}"
        response = await AsyncRequestUtils().get_json(url, headers=headers, timeout=10)
        if response is None:
            raise HTTPException(status_code=502, detail=f"Failed to fetch {endpoint}")
        return response

    def get_subscription_user_info(self) -> DataUsage:
        sub_info = DataUsage()
        for info in self.state.subscription_info.root.values():
            sub_info.upload += info.upload
            sub_info.download += info.download
            sub_info.total += info.total
            sub_info.expire = max(sub_info.expire, info.expire)
        return sub_info

    @staticmethod
    async def async_notify_clash(ruleset: str, api_url: str, api_secret: str):
        """
        通知 Clash 刷新规则集
        """
        logger.info(f"正在刷新 [{ruleset}] {api_url} ...")
        url = f'{api_url}/providers/rules/{ruleset}'
        resp = await AsyncRequestUtils(content_type="application/json",
                                       headers={"authorization": f"Bearer {api_secret}"}
                                       ).put_res(url)
        if resp and resp.status_code == 204:
            logger.info(f"[{ruleset}] {api_url} 刷新完成")
        else:
            logger.warn(f"[{ruleset}] {api_url} 刷新失败")

    def add_notification_job(self, ruleset_names: List[str]):
        if not self.state.config.enabled or not self.scheduler:
            return
        for ruleset in ruleset_names:
            if ruleset in self.state.rule_provider:
                self.scheduler.add_job(
                    ClashRuleProviderService.async_notify_clash, "date",
                    run_date=datetime.now(tz=pytz.timezone(settings.TZ)) +
                             timedelta(seconds=self.state.config.refresh_delay),
                    args=(ruleset, self.state.config.dashboard_url,
                          self.state.config.dashboard_secret),
                    id=f'CRP-notify-clash{ruleset}', replace_existing=True,
                    misfire_grace_time=Constant.MISFIRE_GRACE_TIME
                )

    def build_clash_config(self, param: ConfigRequest) -> ClashConfig | None:
        if not self.state.clash_template:
            config = ClashConfig()
        else:
            config = self.state.clash_template.model_copy(deep=True)

        # Merge subscriptions
        self._merge_subscriptions(config)

        # Add proxies
        config.proxies += self._filter_available_items(self.state.proxies, param)
        config.proxies = list(
            filter(lambda p: not any(kw in p.name for kw in self.state.config.filter_keywords), config.proxies)
        )
        # Add proxy groups
        config.proxy_groups += self._filter_available_items(self.state.proxy_groups, param)

        # Add region groups
        if self.state.config.group_by_region or self.state.config.group_by_country:
            config.proxy_groups += [pg.data for pg in self.proxy_groups_by_region()]

        # Add rule providers (Load once)
        current_rule_providers = self.state.rule_providers
        rule_providers = {}
        for rp_data in current_rule_providers:
            if rp_data.meta.available(param):
                rule_providers[rp_data.name] = rp_data.data
        config.rule_providers = config.rule_providers | rule_providers

        # Apply patches
        config = self._apply_patch_to_config(config)

        # Sync and add auto rule providers
        self.sync_ruleset()
        self._process_auto_rule_providers(config)

        # Add rules (including ACL4SSR)
        self._process_rules(config, param)

        # Add Hosts
        hosts = self.state.hosts.to_dict(self.state.config.best_cf_ip)
        if hosts:
            config.hosts = config.hosts or {}
            config.hosts = config.hosts | hosts

        # Cleanup ruleset names
        self._cleanup_ruleset_names(config)

        # Cycle check
        self._check_cycles(config)

        return config

    def delete_proxy_group(self, name: str) -> Tuple[bool, str]:
        """
        Deletes a proxy group by name and saves the state.
        Returns True if a group was deleted, False otherwise.
        """
        pgs = self.state.proxy_groups
        deleted = pgs.pop(name)
        if deleted:
            self.state.proxy_groups = pgs
            return True, "代理组删除成功"
        return False, f"代理组 {name!r} 不存在"

    def delete_proxy_group_patch(self, name: str) -> tuple[bool, str]:
        patches = self.state.proxy_group_patch
        if name in patches:
            del patches.root[name]
            self.state.proxy_group_patch = patches
            return True, "补丁已删除"
        return False, "补丁不存在"

    def update_proxy_group_meta(self, name: str, meta: Metadata) -> tuple[bool, str]:
        pgs = self.state.proxy_groups
        res = pgs.set_meta(name, meta)
        if res:
            self.state.proxy_groups = pgs
            return True, ""
        return False, f"代理组 {name!r} 不存在"

    def add_proxy_group(self, proxy_group: ProxyGroup) -> tuple[bool, str]:
        """
        Adds a new proxy group, saves the state, and returns status.
        """
        try:
            pgs = self.state.proxy_groups
            pgs.add(ProxyGroupData(data=proxy_group, name=proxy_group.name, meta=Metadata(source=DataSource.MANUAL)))
            self.state.proxy_groups = pgs
        except Exception as e:
            logger.error(f"Failed to add proxy group: {repr(e)}")
            return False, "代理组添加失败"
        return True, "代理组添加成功"

    def get_proxy_groups(self, patched = True) -> list[ProxyGroupData]:
        pgs = self.state.all_proxy_groups
        pgs += self.proxy_groups_by_region()
        if not patched:
            return pgs
        return self._apply_patches(pgs, self.state.proxy_group_patch)

    def update_proxy_group(self, previous_name: str, source: str, proxy_group: ProxyGroup) -> tuple[bool, str]:
        if source == DataSource.MANUAL:
            pgs = self.state.proxy_groups
            pgs.update(previous_name, ProxyGroupData(data=proxy_group, name=proxy_group.name, meta=Metadata()))
            self.state.proxy_groups = pgs
            return True, "代理组更新成功"
        if previous_name != proxy_group.name:
            return False, "请勿修改代理组名称"
        pgs = self.proxy_groups_by_region()
        src = next((g for g in pgs if g.name == previous_name), None)
        if src is None:
            return False, f"代理组 {previous_name!r} ({source}) 不存在"
        self._make_proxy_group_patch(src.data, proxy_group)
        return True, "代理组更新成功"

    def update_rule_provider(self, name: str, rule_provider: RuleProviderData) -> Tuple[bool, str]:
        """
        Updates a rule provider.
        """
        rps = self.state.rule_providers
        if name not in rps:
            return False, f"规则集 {name!r} 不存在"
        rps.update(name, rule_provider)
        self.state.rule_providers = rps
        return True, "规则集更新成功"

    def update_rule_providers_meta(self, name: str, meta: Metadata) -> tuple[bool, str]:
        rps = self.state.rule_providers
        if name in rps:
            res = rps.set_meta(name, meta)
            if res:
                self.state.rule_providers = rps
                return True, ""

        arps = self.state.acl4ssr_providers
        if name in arps:
            res = arps.set_meta(name, meta)
            if res:
                self.state.acl4ssr_providers = arps
                return True, ""
        return False, f"规则集 {name!r} 不存在"

    def update_rule_meta(self, rule_type: RuleSet, priority: int, meta: Metadata) -> tuple[bool, str]:
        manager = self.state.get_rule_manager(rule_type)
        rule = manager.get_rule_at_priority(priority)
        if not rule:
            return False, "规则不存在"
        res = manager.update_rule_meta_at_priority(priority, meta)
        if res:
            if rule_type == RuleSet.RULESET:
                self.add_notification_job([f"{self.state.config.ruleset_prefix}{rule.rule.action}"])
            self.organize_and_save_rules()
            return True, ""
        return False, "更新规则元数据失败"

    def delete_rule_provider(self, name: str) -> tuple[bool, str]:
        rps = self.state.rule_providers
        deleted = rps.pop(name)
        if deleted:
            self.state.rule_providers = rps
            return True, f"规则集删除成功"
        return False, f"规则集 {name!r} 不存在"

    def add_rule_provider(self, name: str, rule_provider: RuleProvider) -> tuple[bool, str]:
        try:
            rps = self.state.rule_providers
            rps.add(RuleProviderData(data=rule_provider, name=name, meta=Metadata(source=DataSource.MANUAL)))
            self.state.rule_providers = rps
        except Exception as e:
            logger.error(f"Failed to add rule provider: {repr(e)}")
            return False, "规则集添加失败"
        return True, "规则集添加成功"

    async def test_connectivity(self, clash_apis: List[ClashApi], sub_links: List[str]) -> Tuple[bool, str]:
        tasks = []
        urls = []
        for d in clash_apis:
            headers = {"authorization": f"Bearer {d.secret}"}
            url = f"{d.url}/version"
            task = asyncio.create_task(
                AsyncRequestUtils(accept_type="application/json", headers=headers, timeout=5).get_res(url)
            )
            urls.append(url)
            tasks.append(task)
        for sub_link in sub_links:
            task = asyncio.create_task(
                AsyncRequestUtils(
                    accept_type="text/html", proxies=settings.PROXY if self.state.config.proxy else None,
                    timeout=5).get(sub_link)
            )
            urls.append(sub_link)
            tasks.append(task)
        results = await asyncio.gather(*tasks)
        for i, result in enumerate(results):
            if not result:
                return False, f"无法连接到 {urls[i]}"
        return True, ""

    def get_status(self) -> Dict[str, Any]:
        data = {
            "state": self.state.config.enabled,
            "ruleset_prefix": self.state.config.ruleset_prefix,
            "preset_identifiers": self.state.config.identifiers,
            "best_cf_ip": self.state.config.best_cf_ip,
            "geoRules": self.state.geo_rules,
            "subscription_info": self.state.subscription_info,
            "sub_url": f"{self.state.config.movie_pilot_url}/api/v1/plugin/{self.plugin_id}/config?"
                       f"apikey={self.state.config.apikey or settings.API_TOKEN}"
        }
        return data

    def get_rules(self, ruleset: RuleSet) -> list[RuleData]:
        manager = self.state.get_rule_manager(ruleset)
        return manager.to_list()

    def reorder_rules(self, rule_type: RuleSet, moved_priority: int, target_priority: int) -> tuple[bool, str]:
        manager = self.state.get_rule_manager(rule_type)
        try:
            rule = manager.reorder_rules(moved_priority, target_priority)
            if rule_type == RuleSet.RULESET:
                self.add_notification_job(
                    [f"{self.state.config.ruleset_prefix}{rule.rule.action}"])
        except Exception as e:
            logger.info(f"Failed to reorder rules: {repr(e)}")
            return False, "规则移动失败"
        self.organize_and_save_rules()
        return True, ""

    def update_rule(self, rule_type: RuleSet, priority: int, rule_data: RuleData) -> tuple[bool, str]:
        try:
            dst_priority = rule_data.priority
            src_priority = priority
            clash_rule = ClashRuleParser.parse_rule_dict(rule_data.model_dump(mode='json', exclude_none=True))
            if not clash_rule:
                return False, f"无效的规则: {rule_data!r}"
            manager = self.state.get_rule_manager(rule_type)
            original_rule = manager.get_rule_at_priority(src_priority)
            meta = Metadata(source=original_rule.meta.source, time_modified=time.time())
            rule_item = RuleItem(rule=clash_rule, meta=meta)
            if rule_type == RuleSet.RULESET:
                res = manager.update_rule_at_priority(rule_item, src_priority, dst_priority)
                if res:
                    ruleset_to_notify = [f"{self.state.config.ruleset_prefix}{clash_rule.action}"]
                    if rule_data.action != original_rule.rule.action:
                        ruleset_to_notify.append(f"{self.state.config.ruleset_prefix}{original_rule.rule.action}")
                    self.add_notification_job(ruleset_to_notify)
            else:
                res = manager.update_rule_at_priority(rule_item, src_priority, dst_priority)
        except Exception as err:
            logger.info(f"Failed to update rules: {repr(err)}")
            return False, "更新规则出错"
        self.organize_and_save_rules()
        return res, ""

    def add_rule(self, rule_type: RuleSet, rule_data: RuleData) -> tuple[bool, str]:
        try:
            priority = rule_data.priority
            clash_rule = ClashRuleParser.parse_rule_dict(rule_data.model_dump(mode='json', exclude_none=True))
            if not clash_rule:
                return False, f"无效的输入规则: {rule_data.model_dump(mode='json', exclude_none=True)}"
            meta = Metadata(source=DataSource.MANUAL, time_modified=time.time())
            rule_item = RuleItem(rule=clash_rule, meta=meta)
            if rule_type == RuleSet.RULESET:
                self.state.ruleset_rules_manager.insert_rule_at_priority(rule_item, priority)
                self.add_notification_job([f"{self.state.config.ruleset_prefix}{clash_rule.action}"])
            else:
                self.state.top_rules_manager.insert_rule_at_priority(rule_item, priority)
        except Exception as err:
            logger.info(f"Failed to add rule: {repr(err)}")
            return False, "添加规则出错"
        self.organize_and_save_rules()
        return True, ""

    def delete_rule(self, ruleset: RuleSet, priority: int):
        manager = self.state.get_rule_manager(ruleset)
        res = manager.remove_rule_at_priority(priority)
        if ruleset == RuleSet.RULESET:
            if res:
                self.add_notification_job([f"{self.state.config.ruleset_prefix}{res.rule.action}"])
        self.organize_and_save_rules()

    def delete_rules(self, ruleset: RuleSet, priorities: list[int]):
        manager = self.state.get_rule_manager(ruleset)
        removed = manager.remove_rules_at_priorities(priorities)
        if ruleset == RuleSet.RULESET:
            if removed:
                actions = {r.rule.action for r in removed}
                self.add_notification_job([f"{self.state.config.ruleset_prefix}{action}" for action in actions])
        self.organize_and_save_rules()

    def set_rules_status(self, ruleset: RuleSet, priorities: dict[int, bool]):
        manager = self.state.get_rule_manager(ruleset)
        updated = manager.update_rules_at_priorities(priorities)
        if ruleset == RuleSet.RULESET:
            if updated:
                actions = {r.rule.action for r in updated}
                self.add_notification_job([f"{self.state.config.ruleset_prefix}{action}" for action in actions])
        self.organize_and_save_rules()

    def import_rules(self, vehicle: str, payload: str) -> tuple[bool, str]:
        rules: List[str] = []
        if vehicle == 'YAML':
            try:
                imported_rules = yaml.load(payload, Loader=yaml.SafeLoader)
                if not isinstance(imported_rules, dict):
                    return False, "无效的输入"
            except yaml.YAMLError as err:
                logger.error(f"Failed to import rules: {repr(err)}")
                return False, 'YAML 格式错误'
            rules = imported_rules.get(ClashKey.RULES, [])
        self.append_top_rules(rules)
        return True, ""

    def get_ruleset(self, name: str) -> Optional[str]:
        ruleset_name = self.state.ruleset_names.get(name)
        if ruleset_name is None:
            return None
        rules = self.ruleset(ruleset_name)
        res = yaml.dump({"payload": rules}, allow_unicode=True)
        return res

    def update_hosts(self, domain: str, host: HostData) -> tuple[bool, str]:
        hosts = self.state.hosts
        hosts.update(domain, host)
        self.state.hosts = hosts
        return True, f"Host for domain {host.domain} updated successfully."

    def delete_host(self, domain: str) -> tuple[bool, str]:
        hosts = self.state.hosts
        original_len = len(hosts)
        hosts.delete(domain)
        if len(hosts) < original_len:
            self.state.hosts = hosts
            return True, ''
        else:
            return False, f'Host for domain {domain} not found.'

    async def refresh_subscription(self, url: str) -> Tuple[bool, str]:
        sub_conf = next((conf for conf in self.state.config.subscriptions_config if conf.url == url), None)
        if not sub_conf:
            return False, f"Configuration for {url} not found."
        config, info = await self.async_get_subscription(url)
        if not config:
            return False, f"订阅链接 {url} 更新失败"

        sub_configs = self.state.sub_configs
        sub_configs[url] = config
        self.state.sub_configs = sub_configs

        sub_info_map = self.state.subscription_info
        info.enabled = sub_info_map.get(url).enabled
        sub_info_map[url] = info
        self.state.subscription_info = sub_info_map
        return True, "订阅更新成功"

    def update_subscription_info(self, sub_setting: SubscriptionSetting):
        sub_info = self.state.subscription_info
        sub_info.set(sub_setting)
        self.state.subscription_info = sub_info

    async def async_get_subscription(self, url: str) -> tuple[ClashConfig | None, SubscriptionInfo | None]:
        if not url:
            return None, None
        logger.info(f"正在刷新 {UtilsProvider.get_url_domain(url)} ...")
        ret = None
        raw_proxies = {}
        for _ in range(self.state.config.retry_times):
            ret = await AsyncRequestUtils(
                accept_type="text/html", timeout=self.state.config.timeout, ua="clash.meta",
                proxies=settings.PROXY if self.state.config.proxy else None
            ).get_res(url)
            if ret:
                break
        if not ret:
            logger.warning(f"{UtilsProvider.get_url_domain(url)} 刷新失败.")
            return None, None
        try:
            content = ret.content
            rs = yaml.safe_load(content)
            if isinstance(rs, str):
                proxies = Converter().convert_v2ray(content)
                if not proxies:
                    raise ValueError("Unknown content type")
                rs = {
                    ClashKey.PROXIES: proxies.values(),
                    ClashKey.PROXY_GROUPS: [
                        {ClashKey.NAME: "All Proxies", 'type': 'select', 'include-all-proxies': True}
                    ]
                }
                raw_proxies = {p['name']: link for link, p in proxies.items()}
            if not isinstance(rs, dict):
                raise ValueError("Subscription content is not a valid dictionary.")
            rs: dict[str, Any] = rs
            logger.info(f"已刷新: {UtilsProvider.get_url_domain(url)}. 节点数量: {len(rs.get(ClashKey.PROXIES, []))}")
            conf = ClashConfig.model_validate(rs)
        except Exception as e:
            logger.error(f"解析配置出错： {e}")
            return None, None
        info = {"last_update": int(time.time()), "proxy_num": conf.node_num}
        if 'Subscription-Userinfo' in ret.headers:
            matches = re.findall(r'(\w+)=(\d+)', ret.headers['Subscription-Userinfo'])
            variables = {key: int(value) for key, value in matches}
            info.update(variables)
        sub_info = SubscriptionInfo(**info)
        conf.raw_proxies = raw_proxies
        return conf, sub_info

    async def async_refresh_subscriptions(self) -> Dict[str, bool]:
        res = {}
        sub_info_map = self.state.subscription_info
        sub_configs_map = self.state.sub_configs
        
        for sub_conf in self.state.config.subscriptions_config:
            url = sub_conf.url
            if not sub_info_map.get(url).enabled:
                continue
            conf, sub_info = await self.async_get_subscription(url)
            if not conf:
                res[url] = False
                continue
            sub_info_map[url] = sub_info
            res[url] = True
            sub_configs_map[url] = conf
        self.state.subscription_info = sub_info_map
        self.state.sub_configs = sub_configs_map
        return res

    async def async_refresh_acl4ssr(self):
        logger.info("正在刷新 ACL4SSR ...")
        paths = ['Clash/Providers', 'Clash/Providers/Ruleset']
        api_url = f"{Constant.ACL4SSR_API}/contents/%s"
        branch = 'master'
        new_providers = []
        names = set()
        for path in paths:
            response = await AsyncRequestUtils().get_res(api_url % path, headers=settings.GITHUB_HEADERS,
                                                         params={'ref': branch})
            if not response:
                continue
            files = response.json()
            yaml_files = [f for f in files if f["type"] == "file" and f[ClashKey.NAME].endswith((".yaml", ".yml"))]
            for f in yaml_files:
                name = f"{self.state.config.acl4ssr_prefix}{f[ClashKey.NAME][:f[ClashKey.NAME].rfind('.')]}"
                if name in names:
                    continue
                file_path = f"./ACL4SSR/{f['name']}"
                provider = RuleProvider(
                    type=VehicleType.HTTP, path=file_path, url=f["download_url"], interval=600, behavior="classical",
                    format="yaml"
                )
                meta = Metadata(source=DataSource.ACL4SSR)
                new_providers.append(RuleProviderData(name=name, data=provider, meta=meta))
                names.add(name)

        self.state.acl4ssr_providers = RuleProviders.model_validate(new_providers)
        logger.info(f"ACL4SSR 规则集刷新完成. 规则集数量: {len(self.state.acl4ssr_providers)}")

    async def async_refresh_geo_dat(self):
        logger.info("正在刷新 Geo Rules ...")
        branch = 'meta'
        api_url = f"{Constant.METACUBEX_RULE_DAT_API}/contents/geo"
        resp = await AsyncRequestUtils().get_res(api_url, headers=settings.GITHUB_HEADERS, params={'ref': branch})
        if not resp:
            return

        geo_rules = self.state.geo_rules
        for path in resp.json():
            if path["type"] == "dir" and path["name"] in geo_rules.model_fields:
                tree_sha = path["sha"]
                url = f"{Constant.METACUBEX_RULE_DAT_API}/git/trees/{tree_sha}"
                res = await AsyncRequestUtils().get_res(url, headers=settings.GITHUB_HEADERS, params={'ref': branch})
                if not res:
                    continue
                tree = res.json()
                yaml_files = [item["path"][:item["path"].rfind('.')] for item in tree["tree"] if
                              item["type"] == "blob" and item['path'].endswith((".yaml", ".yml"))]
                setattr(geo_rules, path["name"], yaml_files)
        self.state.geo_rules = geo_rules
        logger.info(f"Geo Rules 更新完成. 规则数量: "
                    f"geoip({len(self.state.geo_rules.geoip)}), geosite({len(self.state.geo_rules.geosite)})")

    def check_patch_lifetime(self):
        pp = self.state.proxy_patch
        proxies = self.state.all_proxies
        pp.update_patch({g.name for g in proxies}, lifespan=Constant.PATCH_LIFESPAN)
        self.state.proxy_patch = pp

        groups = self.proxy_groups_by_region() + self.state.all_proxy_groups
        pgp = self.state.proxy_group_patch
        pgp.update_patch({g.name for g in groups}, lifespan=Constant.PATCH_LIFESPAN)
        self.state.proxy_group_patch = pgp

        rpp = self.state.rule_provider_patch
        rule_providers = self.state.all_rule_providers
        rpp.update_patch({g.name for g in rule_providers}, lifespan=Constant.PATCH_LIFESPAN)
        self.state.rule_provider_patch = rpp
