"插件市场客户端"

import os
import platform
import shlex
import shutil
import tempfile
import time
import traceback

import requests
import ujson as json

from . import urlmethod
from .cfg import Cfg
from .color_print import Print
from .constants import (
    PLUGIN_MARKET_SOURCE_OFFICIAL,
    TOOLDELTA_CLASSIC_PLUGIN,
    TOOLDELTA_INJECTED_PLUGIN,
)
from .plugin_load import PluginRegData
from .plugin_load.PluginGroup import plugin_group
from .utils import Utils

if platform.system().lower() == "windows":
    CLS_CMD = "cls"
else:
    CLS_CMD = "clear"


def clear_screen() -> None:
    "清屏"
    os.system(shlex.quote(CLS_CMD))


def path_dir(path: str) -> str | None:
    """获取路径的上一级目录

    Args:
        path (str): 路径

    Returns:
        str|None: 上一级目录
    """
    if "/" not in path:
        return None
    return "/".join(path.split("/")[:-1])


def url_join(*urls) -> str:
    """连接 URL

    Returns:
        str: 连接后的 URL
    """
    return "/".join(urls)


def get_json_from_url(url: str) -> dict:
    """从 URL 获取 JSON 数据

    Args:
        url (str): URL

    Raises:
        requests.RequestException: Url 请求失败
        requests.RequestException: 服务器返回了不正确的答复

    Returns:
        dict: JSON 数据
    """
    try:
        resp = requests.get(url, timeout=5)
        resp.raise_for_status()
        return resp.json()
    except requests.RequestException as exc:
        raise requests.RequestException("URL 请求失败") from exc
    except json.JSONDecodeError as exc:
        raise requests.RequestException(
            f"服务器返回了不正确的答复：{resp.text}"
        ) from exc


class PluginMarket:
    "插件市场类"

    def __init__(self):
        self.plugin_id_name_map: dict | None = None
        try:
            self.plugins_download_url = Cfg().get_cfg(
                "ToolDelta基本配置.json", {"插件市场源": str}
            )["插件市场源"]
        except Exception:
            self.plugins_download_url = PLUGIN_MARKET_SOURCE_OFFICIAL

    def enter_plugin_market(self, source_url: str | None = None, in_game=False) -> None:
        """进入插件市场

        Args:
            source_url (str | None, optional): 插件市场源
            in_game (bool, optional): 是否在游戏内
        """
        Print.clean_print("§6正在连接到插件市场..")
        self.plugin_id_name_map = self.get_plugin_id_name_map()
        CTXS = 12
        try:
            if isinstance(source_url, str):
                market_datas = self.get_datas_from_market(source_url)
            market_datas = self.get_datas_from_market()
            plugin_ids_map = self.plugin_id_name_map
            plugins_list = list(market_datas["MarketPlugins"].items())
            all_indexes = len(plugins_list)
            now_index = 0
            sum_pages = int((all_indexes - 1) / CTXS) + 1
            now_page = 0
            last_operation = ""
            while True:
                clear_screen()
                Print.print_inf(
                    market_datas["SourceName"] + ": " + market_datas["Greetings"],
                    need_log=False,
                )
                now_page = int(now_index / CTXS) + 1
                for i in range(now_index, now_index + CTXS):
                    if i in range(all_indexes):
                        plugin_id = plugins_list[i][0]
                        plugin_name = plugin_ids_map[plugin_id]
                        plugin_basic_datas = plugins_list[i][1]
                        if plugin_basic_datas["plugin-type"] == "classic":
                            plugin_type = "类式"
                        elif plugin_basic_datas["plugin-type"] == "injected":
                            plugin_type = "注入式"
                        else:
                            plugin_type = plugin_basic_datas["plugin-type"]
                        Print.print_inf(
                            (f" {i + 1}. §e{plugin_name} §av{plugin_basic_datas['version']} "
                             f"§b@{plugin_basic_datas['author']} §d{plugin_type}插件"),
                            need_log=False,
                        )
                    else:
                        Print.print_inf("")
                Print.print_inf(
                    f"§f第 {now_page} / {sum_pages} 页，输入 §b+§f/§b- §f翻页",
                    need_log=False,
                )
                Print.print_inf("§f输入插件序号选中插件并查看其下载页", need_log=False)
                last_operation = (
                    (
                        input(
                            Print.fmt_info(
                                "§f回车键继续上次操作，§bq§f 退出，请输入：", "§f 输入 "
                            )
                        )
                        or last_operation
                    )
                    .lower()
                    .strip()
                )
                if last_operation == "+":
                    now_index += CTXS
                elif last_operation == "-":
                    now_index -= CTXS
                elif last_operation == "q":
                    break
                else:
                    res = Utils.try_int(last_operation)
                    if res:
                        if res in range(1, all_indexes + 1):
                            plugin_data = self.get_plugin_data_from_market(
                                plugins_list[res - 1][0]
                            )
                            ok, pres = self.choice_plugin(plugin_data)
                            if ok:
                                if in_game:
                                    if (
                                        plugin_data.name
                                        not in plugin_group.loaded_plugins_name
                                    ):
                                        resp = (
                                            input(
                                                Print.fmt_info(
                                                    f"§f可以直接热加载该插件: {plugin_data.name}, 是否加载(§aY§f/§cN§f): "
                                                )
                                            )
                                            .strip()
                                            .lower()
                                        )
                                        if resp == "y":
                                            for i in pres:
                                                if (
                                                    i
                                                    not in plugin_group.loaded_plugins_name
                                                ):
                                                    try:
                                                        plugin_group.load_plugin_hot(
                                                            i.name, i.plugin_type
                                                        )
                                                    except BaseException as err:
                                                        Print.print_err(
                                                            f"插件热加载出现问题：{err}"
                                                        )
                                    else:
                                        Print.print_inf(
                                            "插件已存在，若要更新版本，请重启 ToolDelta",
                                            need_log=False,
                                        )
                                        r = input(
                                            Print.fmt_info(
                                                "§f输入 §cq §f退出, 其他则返回插件市场"
                                            )
                                        )
                                else:
                                    Print.print_inf(
                                        "下载插件后重启 ToolDelta 才能生效",
                                        need_log=False,
                                    )
                                r = input(
                                    Print.fmt_info(
                                        "§f输入 §cq §f退出, 其他则返回插件市场"
                                    )
                                )
                                if r.lower() == "q":
                                    break
                            else:
                                Print.print_inf("已取消。", need_log=False)
                                time.sleep(1)
                        else:
                            Print.print_err("超出序号范围")
                if now_index >= all_indexes:
                    now_index = 0
                elif now_index < 0:
                    now_index = max(now_index - CTXS, 0)
        except KeyError as err:
            Print.print_err(f"获取插件市场插件出现问题：键值对错误：{err}")
            return
        except requests.RequestException as err:
            Print.print_err(f"获取插件市场插件出现问题：{err}")
            return
        except Exception:
            Print.print_err("获取插件市场插件出现问题，报错如下：")
            Print.print_err(traceback.format_exc())
            return
        clear_screen()
        Print.clean_print("§a已从插件市场返回 ToolDelta 控制台。")

    def get_datas_from_market(self, source_url: str | None = None) -> dict:
        """
        从插件市场的 market_tree.json 获取数据

        Args:
            source_url (str | None, optional): 插件市场源，默认为 None, 有则替换整体插件市场源

        Returns:
            dict: 插件市场数据
        """
        if isinstance(source_url, str):
            self.plugins_download_url = self.plugins_download_url
        market_datas = get_json_from_url(
            url_join(self.plugins_download_url, "market_tree.json")
        )
        return market_datas

    def get_plugin_data_from_market(self, plugin_id: str) -> PluginRegData:
        """从插件市场获取单个插件数据

        Args:
            plugin_id (str): 插件 ID

        Raises:
            KeyError: 无法通过 ID 查找插件

        Returns:
            PluginRegData: 插件注册数据
        """
        if self.plugin_id_name_map is None:
            self.plugin_id_name_map = self.get_plugin_id_name_map()
        plugin_name = self.plugin_id_name_map.get(plugin_id)
        if plugin_name is None:
            raise KeyError(f"无法通过 ID: {plugin_id} 查找插件")
        data_url = self.plugins_download_url + "/" + plugin_name + "/datas.json"
        datas = get_json_from_url(data_url)
        return PluginRegData(plugin_name, datas)

    def choice_plugin(
        self, plugin_data: PluginRegData
    ) -> tuple[bool, list[PluginRegData]]:
        """选中插件进行介绍与操作

        Args:
            plugin_data (PluginRegData): 插件注册数据

        Returns:
            tuple[bool, list[PluginRegData]]: 是否下载，下载的插件列表
        """
        pre_plugins_str = (
            ", ".join([f"{k}§7v{v}" for k, v in plugin_data.pre_plugins.items()])
            or "无"
        )
        clear_screen()
        Print.print_inf(
            f"{plugin_data.name} v{plugin_data.version_str}", need_log=False
        )
        Print.print_inf(
            f"作者：§f{plugin_data.author}§7, 版本：§f{plugin_data.version_str} §b{plugin_data.plugin_type_str}",
            need_log=False,
        )
        Print.print_inf(f"前置插件：§f{pre_plugins_str}", need_log=False)
        Print.print_inf(f"介绍：{plugin_data.description}", need_log=False)
        Print.print_inf("", need_log=False)
        res = (
            input(Print.fmt_info("§f下载 = §aY§f, 取消 = §cN§f, 请输入："))
            .lower()
            .strip()
        )
        if res == "y":
            Print.clean_print(f"§6正在下载插件：§f{plugin_data.name}", end="\r")
            pres = self.download_plugin(plugin_data)
            pres.reverse()
            return True, pres
        return False, []

    def get_plugin_id_name_map(self) -> dict:
        """获取插件 ID 与插件名的映射

        Returns:
            dict: 插件 ID 与插件名的映射
        """
        res = requests.get(
            self.plugins_download_url + "/plugin_ids_map.json", timeout=5
        )
        res.raise_for_status()
        res1: dict = json.loads(res.text)
        self.plugin_id_name_map = res1
        return res1

    def download_plugin(
        self, plugin_data: PluginRegData, with_pres=True, is_enabled=True
    ) -> list[PluginRegData]:
        """
        下载插件
        注意：只能传入由 `get_plugin_data_from_market()` 生成的数据

        Args:
            plugin_data (PluginRegData): 插件注册数据
            with_pres (bool): 是否一同下载前置插件
            is_enabled (bool): 下载的插件是否要自动禁用

        Raises:
            ValueError: 未知插件类型

        Returns:
            list[PluginRegData]: 本插件和其前置插件的注册数据列表
        """
        if self.plugin_id_name_map is None:
            self.plugin_id_name_map = self.get_plugin_id_name_map()
        pres = [plugin_data]
        download_paths = self.find_dirs(plugin_data)
        if with_pres:
            for plugin_id in plugin_data.pre_plugins:
                plugin_name = self.plugin_id_name_map[plugin_id]
                Print.clean_print(
                    f"§6正在下载 §f{plugin_data.name}§6 的前置插件 §f{plugin_name}"
                )
                plugin_datas = self.get_plugin_data_from_market(plugin_id)
                pres += self.download_plugin(plugin_datas)
        cache_dir = tempfile.mkdtemp()
        Print.clean_print(
            f"§6正在下载插件 §f{plugin_data.name}§6.." + " " * 15, end="\r"
        )
        match plugin_data.plugin_type:
            case "classic":
                download_path = os.path.join("插件文件", TOOLDELTA_CLASSIC_PLUGIN)
            case "injected":
                download_path = os.path.join("插件文件", TOOLDELTA_INJECTED_PLUGIN)
            case _:
                raise ValueError(
                    f"未知插件类型：{plugin_data.plugin_type}, 你可能需要通知 ToolDelta 项目开发组解决"
                )
        try:
            os.makedirs(os.path.join(cache_dir, plugin_data.name), exist_ok=True)
            all_files_len = len(download_paths)
            for i, paths in enumerate(download_paths):
                Print.clean_print(
                    f"正在下载附带文件 ({i} / {all_files_len}) 请稍后...", end="\r"
                )
                if not paths.strip():
                    # 该路径为空
                    continue
                url = url_join(self.plugins_download_url, paths)
                # 按照插件类型选择下载到的文件夹
                path_last = path_dir(paths)
                if path_last is not None:
                    # 自动创建文件夹
                    folder_path = os.path.join(cache_dir, path_last)
                    os.makedirs(folder_path, exist_ok=True)
                urlmethod.download_unknown_file(url, os.path.join(cache_dir, paths))
            # 将缓存文件夹的插件移动到本文件夹
            target_path = download_path
            os.makedirs(target_path, exist_ok=True)
            # 制作所需的目录结构
            for root, _, files in os.walk(cache_dir):
                for filename in files:
                    source_file = os.path.join(root, filename)
                    target_file = os.path.join(
                        target_path, os.path.relpath(source_file, cache_dir)
                    )
                    os.makedirs(os.path.dirname(target_file), exist_ok=True)
                    shutil.move(source_file, target_file)
            if not is_enabled:
                shutil.rmtree(os.path.join(target_path, plugin_data.name + "+disabled"))
                os.rename(
                    os.path.join(target_path, plugin_data.name),
                    os.path.join(target_path, plugin_data.name + "+disabled"),
                )
            from .plugin_manager import plugin_manager

            plugin_data = self.get_plugin_data_from_market(plugin_data.plugin_id)
            plugin_data.is_enabled = is_enabled
            plugin_manager.push_plugin_reg_data(plugin_data)
            Print.clean_print(
                f"§a成功下载插件 §f{plugin_data.name}§a 至插件文件夹" + " " * 15
            )
        finally:
            shutil.rmtree(cache_dir)
        return pres

    def find_dirs(self, plugin_data: PluginRegData) -> list[str]:
        """查找插件目录

        Args:
            plugin_data (PluginRegData): 插件注册数据

        Raises:
            KeyError: 目录结构错误
            eExceptionrr: 获取插件市场插件目录结构出现问题

        Returns:
            list[str]: 插件目录列表
        """
        try:
            data = get_json_from_url(
                url_join(self.plugins_download_url, "directory.json")
            )
            data_list = []
            for folder, files in data.items():
                if plugin_data.name == folder.split("/")[0]:
                    # 展开
                    for file in files:
                        data_list.append(folder + r"/" + file)
            return data_list
        except KeyError as err:
            Print.print_err(
                f"获取插件市场插件目录结构出现问题：无法找到 {err}, 有可能是未来得及更新目录"
            )
            raise KeyError(f"无法找到 {err}, 有可能是未来得及更新目录") from err
        except Exception as err:
            Print.print_err(f"获取插件市场插件目录结构出现问题：{err}")
            raise KeyError(f"获取插件市场插件目录结构出现问题：{err}") from err

    def get_latest_plugin_version(self, plugin_id: str) -> str:
        """获取最新插件版本

        Args:
            plugin_id (str): 插件 ID

        Raises:
            KeyError: 无法通过 ID 获取最新插件版本

        Returns:
            str: 最新插件版本
        """
        result = get_json_from_url(
            url_join(self.plugins_download_url, "latest_versions.json")
        ).get(plugin_id)
        if isinstance(result, str):
            return result
        raise KeyError(f"无法通过 ID: {plugin_id} 获取最新插件版本")


market = PluginMarket()
