#!/usr/bin/env python3
# coding: utf-8
# Copyright (c) 2024 Huawei Technologies Co., Ltd.
# openUBMC is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
import os
import time
import json
import re
from typing import List
from multiprocessing import Process
from tempfile import NamedTemporaryFile
from packaging import version

from bmcgo.utils.tools import Tools
from bmcgo import misc
from bmcgo.errors import BmcGoException

STAGE_DEV = "dev"
STAGE_PRE = "pre"
STAGE_RC = "rc"
STAGE_STABLE = "stable"

BUILD_TYPE_DT = "DT"
BUILD_TYPE_DEBUG = "Debug"
BUILD_TYPE_RELEASE = "Release"


class DownloadComponentRecipe(Process):
    def __init__(self, tools: Tools, comp, remote_list: list):
        super().__init__()
        self.comp = comp
        self.tools = tools
        self.remote_list = remote_list

    def run(self):
        if misc.conan_v1():
            dep_path = os.path.join(self.tools.conan_data, self.comp.replace("@", "/"))
            if os.path.isdir(dep_path):
                return
        else:
            cmd = f"conan cache path {self.comp}"
            ret = self.tools.run_command(cmd, capture_output=True, ignore_error=True)
            if ret.returncode == 0:
                return
        self.tools.download_conan_recipes(self.comp, self.remote_list)


class ComponentHelper:
    CONAN_PAKCAGE_RE = r"^[^@|/]+/[^@|/]+@[^@|/]+/(rc|stable|dev)$"

    @staticmethod
    def get_language(service_json="mds/service.json", allow_non_service_json=False):
        if not os.path.isfile(service_json):
            if allow_non_service_json:
                return "lua"
            raise RuntimeError(f"{service_json}文件不存在")
        with open(service_json, "r", encoding="UTF-8") as service_fp:
            content = json.load(service_fp)
        return content.get("language", "lua")

    @staticmethod
    def compare_versions(version_str, target_version="1.80.35"):
        # 处理简单版本号格式 a/1.1xxx
        simple_match = re.match(r'[^/]+/(\d+\.\d+(?:\.\d+)?)', version_str)
        if simple_match:
            return version.parse(simple_match.group(1)) >= version.parse(target_version)
        
        # 处理待条件的版本号
        condition_match = re.findall(r'\[([^]]+)\]', version_str)
        if condition_match:
            conditions = condition_match[0].split()

            # 只有>或>=
            if len(conditions) == 1 and any(op in conditions[0] for op in [">", ">="]):
                return True
            
            # 包含<或<=
            for cond in conditions:
                if "<=" in cond:
                    ver = cond.lstrip("<=>")
                    return version.parse(ver) >= version.parse(target_version)
                elif "<" in cond:
                    ver = cond.lstrip("<=>")
                    return version.parse(ver) > version.parse(target_version)
            
            ver = conditions[0].lstrip("<=>")
            return version.parse(ver) >= version.parse(target_version)
        
        return False

    @staticmethod
    def enable_new_major_version(dependencies):
        if not dependencies:
            return False
        
        for dependency in dependencies:
            version_str = dependency.get("conan", "")
            if not version_str.startswith("libmc4lua"):
                continue
            return ComponentHelper.compare_versions(version_str)

        return False

    @staticmethod
    def get_major_version(service_json="mds/service.json"):
        if not os.path.isfile(service_json):
            raise RuntimeError(f"{service_json}文件不存在")
        with open(service_json, "r", encoding="UTF-8") as service_fp:
            content = json.load(service_fp)
        
        return 1 if (ComponentHelper.enable_new_major_version(content.get("dependencies", {}).get("build", {})) or \
                ComponentHelper.enable_new_major_version(content.get("dependencies", {}).get("test", {}))) else 0

    @staticmethod
    def get_config_value(json_data, key: str, default=None):
        for sub_key in key.split("/"):
            json_data = json_data.get(sub_key, None)
            if json_data is None:
                return default
        return json_data

    @staticmethod
    def try_download_once(deps, tools: Tools, remote_list):
        # 先尝试一次下载，防止认证失败
        for comp in deps:
            if misc.conan_v1():
                dep_path = os.path.join(tools.conan_data, comp.replace("@", "/"))
                if os.path.isdir(dep_path):
                    continue
            else:
                cmd = f"conan cache path {comp}"
                ret = tools.run_command(cmd, capture_output=True, ignore_error=True, command_echo=False)
                if ret.returncode == 0:
                    continue
            args = "--only-recipe" if misc.conan_v2() else "--recipe"
            for remote in remote_list:
                try:
                    tools.run_command(f"conan download {comp} -r {remote} {args}", show_log=True, show_error_log=False)
                except Exception as e:
                    tools.log.info(f"Recipe not fount in {remote}: {comp}")
            # 只要执行过下载此时就已经认证，直接返回
            return

    @staticmethod
    def download_recipes(dependencies: List[str], tools: Tools, remote_list):
        pools = []
        ignore_error = os.getenv("CLOUD_BUILD_IGNORE_ERROR")
        # 过滤需要处理的依赖
        deps_to_download = [dep for dep in dependencies if "@" in dep]
        # 先尝试一次下载，防止认证失败
        ComponentHelper.try_download_once(deps_to_download, tools, remote_list)
        for dep in deps_to_download:
            task = DownloadComponentRecipe(tools, dep, remote_list)
            task.start()
            pools.append(task)
            # 减少并发数，避免CI场景连接过多导致服务器无法处理导致的失败
            while len(pools) > 4:
                time.sleep(0.1)
                for pool in pools.copy():
                    if pool.is_alive():
                        continue
                    if pool.exitcode is not None and pool.exitcode != 0 and not ignore_error:
                        tools.log.warning(f"下载组件 ({pool.comp}) 的构建配方(recipe)失败, 退出码: {pool.exitcode}")
                    pools.remove(pool)
        while pools:
            time.sleep(0.1)
            for pool in pools.copy():
                if pool.is_alive():
                    continue
                if pool.exitcode is not None and pool.exitcode != 0 and not ignore_error:
                    raise BmcGoException(f"下载组件 ({pool.comp}) 的构建配方(recipe)失败, 退出码: {pool.exitcode}")
                pools.remove(pool)

    @staticmethod
    def get_all_dependencies(components: List[str], remote) -> List[str]:
        """获取给定的多个组件com/x.yz@user/channel的直接及间接依赖的所有组件及版本。"""
        tools = Tools()
        dependencies = set()
        tempfile = NamedTemporaryFile()
        for comp in components:
            if misc.conan_v1():
                if not re.match(ComponentHelper.CONAN_PAKCAGE_RE, comp):
                    raise BmcGoException(f"组件 {comp} 不符合正则 {ComponentHelper.CONAN_PAKCAGE_RE}")
                cmd = f'conan info "{comp}" --remote {remote} --json {tempfile.name}'
            else:
                if not misc.conan_package_match(comp):
                    raise BmcGoException(f"组件 {comp} 不符合正则 {misc.CONAN_NAME_RESTR}")
                cmd = f'conan graph info --requires="{comp}" --remote {remote} -f json --out-file={tempfile.name}'
            tools.run_command(cmd)

            file_handler = open(tempfile.name, "r")
            conan_comps = json.load(file_handler)
            if misc.conan_v1():
                for conan_comp in conan_comps:
                    comp_ref = conan_comp.get("reference", "")
                    if not comp_ref or comp_ref == comp:
                        continue
                    dependencies.add(comp_ref)
            else:
                deps = conan_comps.get("graph", {}).get("nodes", {}).get("0", {}).get("dependencies", {})
                for _, conan_comp in deps.items():
                    comp_ref = conan_comp.get("ref", "")
                    if not comp_ref or comp_ref == comp:
                        continue
                    dependencies.add(comp_ref)
            file_handler.close()

        return list(dependencies)

    @staticmethod
    def get_user_channel(stage: str):
        if stage == misc.StageEnum.STAGE_DEV.value:
            stage = misc.StageEnum.STAGE_RC.value
        user_channel = f"@{misc.conan_user()}/{stage}"
        return user_channel

    @staticmethod
    def get_full_reference(components: List[str], stage: str):
        user_channel = ComponentHelper.get_user_channel(stage)
        full_reference_comps = []
        for comp in components:
            if "@" not in comp:
                comp += user_channel
            full_reference_comps.append(comp)
        return full_reference_comps