#!/usr/bin/env python
# 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 shutil
import json
import sys
import copy
import re
import tarfile
from conan.tools.files import chdir
from conan.tools.files import copy
from conanbase import ConanBase

required_conan_version = ">=1.60.0"


class AppConan(ConanBase):
    @staticmethod
    def get_relative_path_paths(json_dir, json_path):
        relative_path = os.path.relpath(json_path, json_dir)
        if os.sep == '\\':
            relative_path = relative_path.replace('\\', '/')
        return relative_path.split('/')

    @staticmethod
    def get_sorted_json_files(json_dir):
        json_files = []
        for root, _, files in os.walk(json_dir):
            for file in files:
                if file.endswith(".json") and not os.path.islink(file):
                    file_path = os.path.join(root, file)
                    json_files.append(file_path)
        # 按文件路径排序,确保处理顺序一致
        json_files.sort()
        return json_files

    @staticmethod
    def set_dict_value(keys, k, v, mapper):
        current_dict = mapper
        for key in keys:
            if key not in current_dict:
                current_dict[key] = {}
            current_dict = current_dict[key]
        current_dict[k] = v

    def build(self):
        if self.settings.build_type == "Release":
            with chdir(self, self.source_folder):
                self.run("rm -rf ./interface_config/cli/ipmcset/dft.json")
                self.run("rm -rf ./interface_config/cli/ipmcget/dft.json")
        super(AppConan, self).build()

        with chdir(self, self.source_folder):
            tools_path = os.path.join(self.source_folder, "tools")
            sys.path.append(tools_path)
            from mapper_check import MapperCheck
            MapperCheck.check()
            schema_check = str(self.options.schema_check)
            if schema_check == "true":
                from schema_check.schema_checker import SchemaChecker
                sc = SchemaChecker()
                sc.check()

    def custom_ipmc_version(self):
        bmc_name = str(self.options.bmc_name)
        if bmc_name:
            ipmc_version = os.path.join(self.source_folder, "interface_config/cli/echoes/ipmcget/_version")
            with open(ipmc_version, "r") as fp:
                data = fp.read()
            data = data.replace("openUBMC INFO", f"{bmc_name} INFO")
            new_bmc = bmc_name.ljust(len("openUBMC"))
            data = re.sub(r"\bopenUBMC\b", new_bmc, data)
            with open(ipmc_version, "w") as fp:
                fp.write(data)

    def read_config(self):
        config_path = os.path.join(self.source_folder, "interface_config/redfish/config.json")
        with open(config_path, 'r') as file:
            config = json.load(file)
        # 获取SoftwareName和SmsName的值,如果没有配置则使用默认值
        software_name = config.get('GlobalVariable', {}).get('SoftwareName', 'openUBMC')
        sms_name = config.get('GlobalVariable', {}).get('SmsName', 'BMA')
        return software_name, sms_name

    def replace_strings_in_files(self, software_name, sms_name):
        schemastore_path = os.path.join(self.source_folder,
                                        "interface_config/redfish/static_resource/redfish/v1/schemastore/en")
        # 只遍历 dmtf/json_schema 和 oem/*/json_schema,避免处理 en/ 下作为硬链接的 json 文件
        json_schema_dirs = [
            os.path.join(schemastore_path, "dmtf/json_schema"),
        ]
        # 添加 oem 厂商目录下的 json_schema
        oem_path = os.path.join(schemastore_path, "oem")
        if os.path.exists(oem_path):
            for oem_vendor in os.listdir(oem_path):
                oem_json_schema = os.path.join(oem_path, oem_vendor, "json_schema")
                if os.path.exists(oem_json_schema):
                    json_schema_dirs.append(oem_json_schema)

        for json_schema_dir in json_schema_dirs:
            if not os.path.exists(json_schema_dir):
                continue
            for root, _, files in os.walk(json_schema_dir):
                for filename in files:
                    if filename.endswith('.json'):
                        file_path = os.path.join(root, filename)
                        with open(file_path, 'r') as file:
                            content = file.read()
                        # 批量字段替换
                        content = content.replace('{{SoftwareName}}', software_name)
                        content = content.replace('{{SmsName}}', sms_name)
                        # 写回静态文件
                        with open(file_path, 'w') as file:
                            file.write(content)

        # 在 schemastore/en/ 下创建硬链接
        def create_hard_links(src_dir):
            """在 schemastore/en/ 下创建指向 src_dir 的硬链接"""
            src_path = os.path.join(schemastore_path, src_dir)
            if not os.path.exists(src_path):
                return
            for filename in os.listdir(src_path):
                if filename.endswith('.json'):
                    src_file = os.path.join(src_path, filename)
                    dst_file = os.path.join(schemastore_path, filename)
                    # 如果目标文件已存在且不是硬链接,先删除
                    if os.path.exists(dst_file):
                        src_stat = os.stat(src_file)
                        dst_stat = os.stat(dst_file)
                        if src_stat.st_ino != dst_stat.st_ino:
                            os.remove(dst_file)
                    # 如果目标不存在,创建硬链接
                    if not os.path.exists(dst_file):
                        os.link(src_file, dst_file)

        # dmtf/json_schema 和 oem/openubmc/json_schema 下的所有 json 文件
        for schema_dir in ["dmtf/json_schema", "oem/openubmc/json_schema"]:
            create_hard_links(schema_dir)

    def json_to_lua_table(self, data, indent=0):
        lua = ""
        if isinstance(data, dict):
            if not data:
                return "{}"
            lua += "{"
            for key, value in data.items():
                lua += f'["{key}"]={self.json_to_lua_table(value, indent + 1)},'
            lua += f'}}'
        elif isinstance(data, list):
            if not data:
                return "{}"
            lua += "{"
            for i, value in enumerate(data, start=1):
                lua += f'[{i}]={self.json_to_lua_table(value, indent + 1)},'
            lua += f'}}'
        elif isinstance(data, str):
            # 处理字符串中的特殊字符
            escaped_str = data.strip('"').replace('\\', '\\\\').replace('"', '\\"')
            escaped_str = escaped_str.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
            lua += f'"{escaped_str}"'
        elif isinstance(data, bool):
            lua += "true" if data else "false"
        elif isinstance(data, (int, float)):
            lua += str(data)
        elif data is None:
            # 映射配置中读取到值为None时,转换为cjson.null,避免与lua nil混淆
            lua += "cjson.null"
        else:
            raise TypeError(f"Unsupported data type: {type(data)}")
        return lua

    def json_to_lua(self, intf, json_dir, lua_file_path, lua_mapper_file_path, lua_route_tree_path):
        json_files = self.get_sorted_json_files(json_dir)
        lua_content = "Input.data="
        mapper_content = "Input.data="
        lua = {}
        mapper = {}
        if intf == "cli":
            route_tree_content = "Input.data="
            route_tree = {}
        for json_file in json_files:
            with open(json_file, 'r') as f:
                data = json.load(f)
            relative_path_list = self.get_relative_path_paths(json_dir, json_file)

            for resource in data.get("Resources", []):
                uri = resource.get("Uri")
                if uri is None:
                    continue
                # redfish接口需要获取uri的IgnoreEtags配置,默认为空列表
                if intf == "redfish":
                    ignore_etags = resource.get("IgnoreEtags", [])
                    self.set_dict_value(relative_path_list + [uri], "IgnoreEtags", ignore_etags, mapper)
                for key, value in resource.items():
                    if key in ["Uri", "IgnoreEtags", "Interfaces"]:
                        continue
                    self.set_dict_value([uri], key, value, lua)
                for interface in resource.get("Interfaces", []):
                    method_type = interface.get("Type")
                    if method_type is None:
                        continue
                    method_type = method_type.lower()
                    self.set_dict_value(relative_path_list + [uri], method_type, True, mapper)
                    if intf == "cli":
                        self.set_dict_value(uri.split('/')[1:] + ['#methods'], method_type, True, route_tree)
                    lock_down_allow = interface.get("LockDownAllow")
                    if lock_down_allow is not None and method_type == 'get':
                        lock_down_allow = True
                    if lock_down_allow is not None:
                        lua_str = self.json_to_lua_table(lock_down_allow, 4)
                        self.set_dict_value([uri, 'methods', method_type], 'LockDownAllow', lua_str, lua)
                    rsp_body = interface.get("RspBody")
                    # redfish接口响应体需要保序,转为字符串
                    if rsp_body is not None and intf == 'redfish':
                        self.set_dict_value([uri, 'methods', method_type], 'RspBody', json.dumps(rsp_body), lua)
                    elif rsp_body is not None:
                        self.set_dict_value([uri, 'methods', method_type], 'RspBody', rsp_body, lua)
                    req_body = interface.get("ReqBody")
                    if req_body is not None and intf == 'cli':
                        self.set_dict_value([uri, 'methods', method_type], 'ReqBody', json.dumps(req_body), lua)
                    elif req_body is not None:
                        self.set_dict_value([uri, 'methods', method_type], 'ReqBody', req_body, lua)
                    action_rsp_body = interface.get("ActionResponseBody")
                    if action_rsp_body is not None:
                        lua_str = self.json_to_lua_table(json.dumps(action_rsp_body), 4)
                        self.set_dict_value([uri, 'methods', method_type], 'ActionResponseBody', lua_str, lua)
                    processing_flow = interface.get("ProcessingFlow")
                    if processing_flow is not None:
                        for pf_item in processing_flow:
                            # 对象结构的CallIf有前后依赖关系,需要转为字符串
                            if "CallIf" in pf_item:
                                pf_item["CallIf"] = json.dumps(pf_item["CallIf"])
                    for key, value in interface.items():
                        if key in ["Type", "RspBody", "ReqBody", "ActionResponseBody", "LockDownAllow"]:
                            continue
                        self.set_dict_value([uri, 'methods', method_type], key, value, lua)
        mapper_content += f"{self.json_to_lua_table(mapper, 0)}"
        lua_content += f"{self.json_to_lua_table(lua, 0)}"
        if intf == 'cli':
            route_tree_content += f"{self.json_to_lua_table(route_tree, 0)}"
            directory = os.path.dirname(lua_route_tree_path)
            if not os.path.exists(directory):
                os.makedirs(directory)
            with open(lua_route_tree_path, 'w') as f:
                f.write(route_tree_content)
                self.output.info('{} package precompilation route tree file to {} successfully'\
                            .format(intf, lua_route_tree_path))
        directory = os.path.dirname(lua_file_path)
        if not os.path.exists(directory):
            os.makedirs(directory)
        with open(lua_file_path, 'w') as f:
            f.write(lua_content)
            self.output.info('{} package precompilation config file to {} successfully'.format(intf, lua_file_path))
        directory = os.path.dirname(lua_mapper_file_path)
        if not os.path.exists(directory):
            os.makedirs(directory)
        with open(lua_mapper_file_path, 'w') as f:
            f.write(mapper_content)
            self.output.info('{} package precompilation mapper file to {} successfully'\
                            .format(intf, lua_mapper_file_path))

    def package(self):
        copy(self, "permissions.ini", src=os.path.join(self.source_folder, "dist"), dst=self.package_folder)
        self.custom_ipmc_version()
        software_name, sms_name = self.read_config()
        self.replace_strings_in_files(software_name, sms_name)

        super(AppConan, self).package()
        sys.path.append(self.source_folder)
        from  custom import mapping_config_patch

        # 各个接口的Script和Plugins配置文件需要保持文本形式,避免探测关联属性失败以及映射器批量替换字段失败
        interface = {"redfish", "web_backend", "cli", "snmp"}
        for intf in interface:
            lua_dir = os.path.join(self.source_folder, "interface_config/" + intf)
            lua_package_dir = os.path.join(self.package_folder, "opt/bmc/apps/" + intf + "/interface_config")
            for root, dirs, files in os.walk(lua_dir):
                for file in files:
                    if file.endswith(".lua") and not os.path.islink(file):
                        file_path = os.path.join(root, file)
                        file_package_path = file_path.replace(lua_dir, lua_package_dir)
                        shutil.copy2(file_path, file_package_path)
            config_path = os.path.join(self.source_folder, 'interface_config', intf)
            patch_path = os.path.join(self.source_folder, 'oem', str(self.options.oem), intf)
            if not os.path.isdir(patch_path):
                continue
            map_patch = mapping_config_patch.MappingConfigPatch(self, patch_path, config_path)
            map_patch.run()

        # 支持装备测试正向采集
        for intf in ["board_info_collector"]:
            lua_dir = os.path.join(self.source_folder, "interface_config/" + intf)
            lua_package_dir = os.path.join(self.package_folder, "opt/bmc/board_info_collector")
            if not os.path.exists(lua_package_dir):
                os.mkdir(lua_package_dir)
            for root, dirs, files in os.walk(lua_dir):
                for file in files:
                    if file.endswith(".lua") and not os.path.islink(file):
                        file_path = os.path.join(root, file)
                        file_package_path = file_path.replace(lua_dir, lua_package_dir)
                        result =os.path.split(file_package_path)
                        if not os.path.exists(result[0]):
                            os.mkdir(result[0])
                        shutil.copy2(file_path, file_package_path)
            config_path = os.path.join(self.source_folder, 'interface_config', intf)
            patch_path = os.path.join(self.source_folder, 'oem', str(self.options.oem), intf)
            if not os.path.isdir(patch_path):
                continue
            map_patch = mapping_config_patch.MappingConfigPatch(self, patch_path, config_path)
            map_patch.run()

        # 支持各个接口映射配置预编译
        interface_mapper = {
            "redfish": ["mapping_config"],
            "web_backend": ["mapping_config"],
            "cli": ["ipmcget", "ipmcset"],
            "snmp": ["mapping_config"],
            "board_info_collector": ["config"]
        }
        for intf, sub_dirs in interface_mapper.items():
            for sub_dir in sub_dirs:
                json_dir = os.path.join(self.source_folder, "interface_config", intf, sub_dir)
                if intf == "board_info_collector":
                    lua_package_dir = os.path.join(self.package_folder, "opt/bmc/board_info_collector")
                else:
                    lua_package_dir = os.path.join(self.package_folder, "opt/bmc/apps", intf, "interface_config")
                lua_file_path = os.path.join(lua_package_dir, sub_dir, "config.lua")
                lua_mapper_file_path = os.path.join(lua_package_dir, sub_dir, "mapper.lua")
                if intf == "cli":
                    lua_route_tree_path = os.path.join(lua_package_dir, sub_dir, "route_tree.lua")
                    self.json_to_lua(intf, json_dir, lua_file_path, lua_mapper_file_path, lua_route_tree_path)
                else:
                    self.json_to_lua(intf, json_dir, lua_file_path, lua_mapper_file_path, None)

        # 压缩json文件(保留硬链接)
        config_dir = os.path.join(self.source_folder, "interface_config")
        inode_map = {}
        for root, dirs, files in os.walk(config_dir):
            for file in files:
                file_full_path = os.path.join(root, file)
                if file.endswith(".json") and not os.path.islink(file_full_path):
                    file_path = file_full_path
                    with open(file_path, mode='rb') as fp:
                        data = json.load(fp)
                    content = json.dumps(data, separators=(',', ':'))
                    with open(file_path, mode='wb') as fd:
                        fd.write(content.encode('utf-8'))
                    for interface_type in ["web_backend", "redfish", "snmp", "cli"]:
                        if "interface_config/" + interface_type in file_path:
                            json_dir = os.path.join(self.source_folder, "interface_config", interface_type)
                            json_package_dir = os.path.join(self.package_folder, "opt/bmc/apps", interface_type, "interface_config")
                    for interface_type in ["board_info_collector"]:
                        if "interface_config/" + interface_type in file_path:
                            json_dir = os.path.join(self.source_folder, "interface_config", interface_type)
                            json_package_dir = os.path.join(self.package_folder, "opt/bmc/board_info_collector")
                    file_package_path = file_path.replace(json_dir, json_package_dir)
                    result = os.path.split(file_package_path)
                    if not os.path.exists(result[0]):
                        os.makedirs(result[0])

                    # 保留硬链接的复制逻辑
                    file_stat = os.stat(file_path)
                    inode_key = (file_stat.st_dev, file_stat.st_ino)

                    if inode_key in inode_map:
                        # 检查目标文件是否已存在
                        if os.path.exists(file_package_path):
                            # 检查是否已经是正确的硬链接
                            src_stat = os.stat(inode_map[inode_key])
                            try:
                                dst_stat = os.stat(file_package_path)
                                if src_stat.st_ino == dst_stat.st_ino:
                                    continue  # 已经是正确的硬链接,跳过
                            except OSError:
                                pass
                            # 目标存在但不是正确的硬链接,删除后重建
                            os.remove(file_package_path)
                        os.link(inode_map[inode_key], file_package_path)
                    else:
                        shutil.copy2(file_path, file_package_path)
                        inode_map[inode_key] = file_package_path

        # 压缩xml文件
        xml_dir = os.path.join(self.source_folder, "interface_config/redfish/static_resource/redfish/v1")
        for root, _, files in os.walk(xml_dir):
            for file in files:
                file_full_path = os.path.join(root, file)
                if file.endswith(".xml") and not os.path.islink(file_full_path):
                    file_path = file_full_path
                    with open(file_path, mode='r', encoding='utf-8') as fp:
                        content = fp.read()
                    # 压缩XML:移除多余空格换行
                    content = self.compress_xml(content)
                    with open(file_path, mode='w', encoding='utf-8') as fd:
                        fd.write(content)
                    # 复制到打包目录
                    xml_package_dir = os.path.join(self.package_folder, "opt/bmc/apps/redfish/interface_config/static_resource/redfish/v1")
                    file_package_path = file_path.replace(xml_dir, xml_package_dir)
                    result = os.path.split(file_package_path)
                    if not os.path.exists(result[0]):
                        os.makedirs(result[0])
                    shutil.copy2(file_path, file_package_path)

        # 将 jsonschemas 和 schemastore 目录压缩为 .tar.bz2 压缩包
        static_v1_src = os.path.join(self.source_folder,
                                     "interface_config/redfish/static_resource/redfish/v1")
        static_v1_dst = os.path.join(self.package_folder,
                                     "opt/bmc/apps/redfish/interface_config/static_resource/redfish/v1")
        for dirname in ["jsonschemas", "schemastore"]:
            src_dir = os.path.join(static_v1_src, dirname)
            if not os.path.exists(src_dir):
                continue
            tar_name = dirname + ".tar.bz2"
            tar_dst_path = os.path.join(static_v1_dst, tar_name)
            if not os.path.exists(static_v1_dst):
                os.makedirs(static_v1_dst)
            with tarfile.open(tar_dst_path, "w:bz2") as tar:
                self._add_dir_to_tar_with_hardlinks(tar, src_dir, dirname)
            # 删除打包目录中的原始目录
            remove_dir = os.path.join(static_v1_dst, dirname)
            if os.path.exists(remove_dir):
                shutil.rmtree(remove_dir)

    def compress_xml(self, xml_content):
        # 压缩XML内容为单行,移除多余空格
        # 移除所有换行符
        xml_content = xml_content.replace('\n', '').replace('\r', '')

        # 移除标签之间的多余空格(保留单个空格)
        xml_content = re.sub(r'\s+', ' ', xml_content)

        # 移除>后的空格和<前的空格
        xml_content = re.sub(r'>\s+', '>', xml_content)
        xml_content = re.sub(r'\s+<', '<', xml_content)

        return xml_content

    def package_info(self):
        pass

    def _add_dir_to_tar_with_hardlinks(self, tar, src_dir, arcname):
        inode_map = {}

        for root, dirs, files in os.walk(src_dir):
            dirs.sort()
            rel_dir = os.path.relpath(root, src_dir)
            if rel_dir == '.':
                arc_dir = arcname
            else:
                arc_dir = arcname + '/' + rel_dir.replace(os.sep, '/')

            tarinfo = tar.gettarinfo(root, arcname=arc_dir)
            tar.addfile(tarinfo)

            for filename in sorted(files):
                filepath = os.path.join(root, filename)
                arc_filepath = arc_dir + '/' + filename

                if os.path.islink(filepath):
                    # 符号链接直接添加
                    tarinfo = tar.gettarinfo(filepath, arcname=arc_filepath)
                    tar.addfile(tarinfo)
                    continue

                stat = os.lstat(filepath)
                inode_key = (stat.st_dev, stat.st_ino)

                if stat.st_nlink > 1 and inode_key in inode_map:
                    # 硬链接:创建LNKTYPE条目,指向归档中首次出现的文件
                    tarinfo = tarfile.TarInfo(name=arc_filepath)
                    tarinfo.type = tarfile.LNKTYPE
                    tarinfo.linkname = inode_map[inode_key]
                    tarinfo.mtime = stat.st_mtime
                    tarinfo.mode = stat.st_mode
                    tarinfo.uid = stat.st_uid
                    tarinfo.gid = stat.st_gid
                    tar.addfile(tarinfo)
                else:
                    # 首次遇到或无硬链接,添加完整文件内容
                    tarinfo = tar.gettarinfo(filepath, arcname=arc_filepath)
                    with open(filepath, 'rb') as f:
                        tar.addfile(tarinfo, f)
                    if stat.st_nlink > 1:
                        inode_map[inode_key] = arc_filepath