# Copyright (c) 2024-2025 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 subprocess
import json
import re
import os
import shutil
import site
import logging
from collections import defaultdict
from datetime import datetime, timezone
from conanbase import ConanBase, copy

required_conan_version = ">=1.60.0"

PATH_CONFLICT_IGNORE_LIST = [
    "/bmc/kepler/IpmiService",
    "/bmc/kepler/Systems/:SystemId/Thermal/Fans",
    "/bmc/ByteDance/Managers/:ManagerId",
]
INVALID_DEBUG_PATH_INTF = {
    "/bmc/kepler/Systems/:SystemId/DataAcquisitionService": [
        "bmc.kepler.Debug.DataAcquisitionService"
    ],
    "/bmc/kepler/Systems/:SystemId/DiskFaultPrediction": [
        "bmc.kepler.Debug.DiskFaultPrediction"
    ],
    "/bmc/kepler/Systems/${SystemId}/FDMDomain/${Id}": [
        "bmc.kepler.Debug.Systems.FDMDomain",
        "bmc.kepler.Debug.Systems.ExceptionInfo",
    ],
    "/bmc/kepler/Systems/${SystemId}/FDMDomain/BoardRAS/${Id}": [
        "bmc.kepler.Debug.Systems.FDMDomain.BoardRAS"
    ],
    "/bmc/kepler/Systems/:SystemId/Mctp": ["bmc.kepler.Debug.Mctp"],
    "/bmc/kepler/Systems/:SystemId/PowerConverter/:ID": ["bmc.kepler.Debug.BlackBox"],
    "/bmc/kepler/Systems/:SystemId/Storage": [
        "bmc.kepler.Debug.Storage.PhyBitError",
        "bmc.kepler.Debug.Storage.RecordDriveSpareBlock",
    ],
}

cwd = os.path.join(os.getcwd(), os.path.dirname(__file__))

class MdbInterfaceConan(ConanBase):
    def run_command(self, cmd):
        logging.info("Run command ({})".format(cmd))
        subprocess.run(cmd, shell=True, check=True)

    def package(self):
        super().package()
        copy(self, "permissions.ini", src=os.path.join(self.source_folder, "dist"), dst=self.package_folder)


    def get_service_value(self, key: str):
        service = None
        if not os.path.isfile(self.srv_file):
            return service
        file = open(self.srv_file, mode="r")
        service = json.load(file)
        file.close()
        if service is None:
            return service
        for sub_key in key.split("/"):
            service = service.get(sub_key, None)
            if service is None:
                return service
        return service

    @staticmethod
    def is_camel_case(input_string):
        # 检查字符串是否以大写字母开头
        if input_string[0].isupper() == False:
            return False

        # 检查字符串是否包含下划线
        if "_" in input_string:
            return False

        # 检查字符串是否只包含字母和数字
        if input_string.isalnum() == False:
            return False

        # 如果上面的所有检查都通过，则说明字符串符合大驼峰风格
        return True

    @staticmethod
    def match_placeholders(msg):
        return sorted(set(int(v) for v in re.findall(r"%(\d+)", msg)))

    @staticmethod
    def check_placeholders(message_id, message_data):
        placeholders = MdbInterfaceConan.match_placeholders(message_data['Message'])
        if message_data.get("NumberOfArgs", 0) != len(placeholders):
            raise Exception(f"Ensure the message {message_id} has correct number of args")

    @staticmethod
    def check_content(content):
        for message_id, message_data in content.items():
            if not MdbInterfaceConan.is_camel_case(message_id):
                raise Exception(f"Ensure the message id {message_id} is big hump style")

            MdbInterfaceConan.check_placeholders(message_id, message_data)

    @staticmethod
    def check_duplicate_keys(items):
        result = {}
        for k, v in items:
            if k in result:
                raise Exception(f"Duplicate message id: {k}")
            result[k] = v
        return result

    def check_messages(self, build_dir):
        messages_dir = os.path.join(build_dir, 'messages')
        for dirpath, _, filenames in os.walk(messages_dir):
            for filename in filenames:
                file_path = os.path.join(dirpath, filename)
                with open(file_path) as fp:
                    suffix = os.path.relpath(file_path, build_dir)
                    if suffix == 'messages/init.json':
                        raise Exception(f"The name of message file init.json is a reserved field, " +
                            "please modify the filename")
                    content = json.load(fp, object_pairs_hook=self.check_duplicate_keys)['Messages']
                    self.check_content(content)

    @staticmethod
    def make_prefix(lang):
        prefix = ['-- ', '-- ', '', '--']  # lua style is default
        if lang in ['c', 'cpp', 'java', 'rust', 'proto']:
            prefix = ['/* ', ' * ', ' */\n', ' *']  # c style for C/C++/proto etc.
        elif lang in ['python', 'shell']:
            prefix = ['#\! ', ' # ', '', ' #']  # python style for shell/python
        return prefix

    @staticmethod
    def formatter(header, date, lang, draft_info):
        if draft_info:
            author = '<change to your name>'
            create = '<模板自动生成初稿, 需要您进行编辑>'
            description = draft_info
        else:
            author = 'auto generate'
            create = date
            description = f'DO NOT EDIT;'

        return header.format(date[:4], author, create, description,
                             MdbInterfaceConan.make_prefix(
                                 lang)[1], MdbInterfaceConan.make_prefix(lang)[0],
                             MdbInterfaceConan.make_prefix(lang)[2], MdbInterfaceConan.make_prefix(lang)[3])

    @staticmethod
    def get_create_date(target_file):
        if os.path.exists(target_file):
            file = open(target_file, "r")
            content = file.read()
            file.close()
            date = re.search(r"Create: (\d+)-(\d+)-(\d+)", content)
            if date:
                return date[1], date[2], date[3]
        now = datetime.now(tz=timezone.utc)
        return now.year, now.month, now.day

    @staticmethod
    def make_header(target_file, draft_info=""):
        year, month, day = MdbInterfaceConan.get_create_date(target_file)
        date = f'{year}-{month}-{day}'
        header = '''{5}Copyright (c) Huawei Technologies Co., Ltd. {0}-{0}. All rights reserved.
{7}
{4}this file licensed under the Mulan PSL v2.
{4}You can use this software according to the terms and conditions of the Mulan PSL v2.
{4}You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
{7}
{4}THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
{4}IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
{4}PURPOSE.
{4}See the Mulan PSL v2 for more details.
{7}
{4}Author: {1}
{4}Create: {2}
{4}Description: {3}
{6}'''
        return lambda lang: MdbInterfaceConan.formatter(header, date, lang, draft_info)

    @staticmethod
    def get_all_files(path):
        """
        获取目录及其子目录下所有文件的路径列表
        """
        file_list = []
        dir_list = sorted(os.listdir(path))
        for file_name in dir_list:
            file_path = os.path.join(path, file_name)
            if os.path.isdir(file_path):
                file_list.extend(MdbInterfaceConan.get_all_files(file_path))
            else:
                file_list.append(file_path)
        return file_list

    @staticmethod
    def is_placeholder(segment):
        """检查路径段是否是占位符（以':'开头或'${'开头'}'结尾）"""
        return segment.startswith(':') or (segment.startswith('${') and segment.endswith('}'))

    @staticmethod
    def compare_segments(seg1, seg2):
        """
        比较两个路径段：
        - 返回类型是否等价（占位符或字符串忽略大小写后相同）
        - 返回实际字符串是否相同
        """
        p1 = MdbInterfaceConan.is_placeholder(seg1)
        p2 = MdbInterfaceConan.is_placeholder(seg2)
        if p1 and p2:
            # 两个都是占位符：类型等价，实际字符串不同（占位符格式不同）
            return True, False
        elif not p1 and not p2:
            # 两个都是字符串：检查忽略大小写后是否相同，以及实际字符串是否相同
            return (seg1.lower() == seg2.lower()), (seg1 == seg2)
        else:
            # 一个占位符一个字符串：类型不等价
            return False, False

    @staticmethod
    def check_path_conflict(seg_list1, seg_list2):
        """
        检查两条路径是否冲突：
        - 段数相同时：所有段类型等价则冲突
        - 段数不同时：公共前缀类型等价且存在实际大小写不同的字符串段则冲突
        """
        m, n = len(seg_list1), len(seg_list2)
        k = min(m, n)
        type_eq_all = True  # 所有段类型等价
        has_actual_diff = False  # 存在实际大小写不同的字符串段

        # 比较公共前缀部分
        for i in range(k):
            seg1, seg2 = seg_list1[i], seg_list2[i]
            type_eq, actual_eq = MdbInterfaceConan.compare_segments(seg1, seg2)
            if not type_eq:
                type_eq_all = False
                break
            elif not MdbInterfaceConan.is_placeholder(seg1) and not MdbInterfaceConan.is_placeholder(seg2) and \
                seg1 != seg2:
                has_actual_diff = True

        # 根据段数判断冲突
        if m == n:
            return type_eq_all
        else:
            return type_eq_all and has_actual_diff

    @staticmethod
    def check_debug_intferface(path, interfaces):
        for interface in interfaces:
            if path in INVALID_DEBUG_PATH_INTF and interface in INVALID_DEBUG_PATH_INTF[path]:
                continue
            if re.match(r'^bmc\.[^.]+\.Debug', interface) and re.match(r'^/bmc/[^/]+/Debug', path) is None:
                raise Exception(
                    f"The debug interface {interface} is not allowed to be defined in the non-debug path {path}"
                )

    @staticmethod
    def check_mdb_path(base_dir):
        """检查指定目录下的所有JSON文件中定义的路径是否存在冲突"""
        json_dir = os.path.join(base_dir, "json", "path")
        path_entries = []  # 存储所有路径信息：(分割段列表, 原始路径, 文件路径)

        # 收集所有JSON文件中的路径定义
        for root, _, files in os.walk(json_dir):
            for file in files:
                if file.endswith('.json'):
                    full_path = os.path.join(root, file)
                    with open(full_path, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                    for resource_def in data.values():
                        if isinstance(resource_def, dict) and 'path' in resource_def:
                            path_str = resource_def['path']
                            if 'interfaces' in resource_def:
                                MdbInterfaceConan.check_debug_intferface(path_str, resource_def['interfaces'])
                            segments = path_str.strip('/').split('/')  # 分割路径并移除首尾'/'
                            path_entries.append((segments, path_str, full_path))

        # 两两比较所有路径
        n = len(path_entries)
        for i in range(n):
            seg_list1, raw1, file1 = path_entries[i]
            for j in range(i + 1, n):
                seg_list2, raw2, file2 = path_entries[j]
                if MdbInterfaceConan.check_path_conflict(seg_list1, seg_list2):
                    if raw1 in PATH_CONFLICT_IGNORE_LIST:
                        continue
                    raise ValueError(
                        f"Path conflict detected:\n"
                        f"  Path 1: {raw1} (in {file1})\n"
                        f"  Path 2: {raw2} (in {file2})"
                    )

    def gen_messages_entry(self, build_dir):
        gen_messages_dir = os.path.join(build_dir, 'messages_gen')
        gen_file = os.path.join(gen_messages_dir, 'init.lua')
        with open(gen_file, "w") as fp:
            header = MdbInterfaceConan.make_header(gen_file)('lua')
            fp.write(f"{header}\n")
            fp.write("local M = {}\n\n")
            file_list = MdbInterfaceConan.get_all_files(gen_messages_dir)
            for filename in file_list:
                suffix = os.path.relpath(filename, gen_messages_dir).split(".")[0]
                if suffix == 'init':
                    continue
                fp.write("M.{} = require 'messages.{}'\n".format(suffix.replace('/', '_'), 
                    suffix.replace('/', '.')))
            fp.write("\nreturn M\n")

    def package_info(self):
        pass

    def find_bmcgo_dir(self):
        from bmcgo.codegen import lua
        return os.path.dirname(lua.__file__)

    def parse_interface_path_mappings(self, root_dir):
        """
        解析指定目录及其子目录下所有JSON文件中的interface-path映射关系
        """
        # 使用集合自动去重并保存所有唯一的path
        interface_paths = defaultdict(set)
        path_dir = os.path.join(root_dir, "json", "path")
        # 遍历目录及子目录下的所有文件
        for root, _, files in os.walk(path_dir):
            for filename in files:
                file_path = os.path.join(root, filename)
                try:
                    with open(file_path, 'r', encoding='utf-8') as f:
                        data = json.load(f)
                except (json.JSONDecodeError, UnicodeDecodeError):
                    continue  # 跳过非JSON文件或无效文件

                # 遍历每个顶级对象
                for obj in data.values():
                    if 'path' in obj and 'interfaces' in obj:
                        path = obj['path']
                        interfaces = obj['interfaces']

                        # 将路径添加到每个接口对应的集合中
                        for interface in interfaces:
                            interface_paths[interface].add(path)

        # 转换集合为字符串或数组
        result = {}
        for interface, paths in interface_paths.items():
            result[interface] = sorted(list(paths)) if len(paths) > 1 else next(iter(paths))

        mdb_config_path = os.path.join(root_dir, "mdb_config.json")
        # 写入JSON文件
        with open(mdb_config_path, 'w', encoding='utf-8') as f:
            json.dump({k: result.get(k) for k in sorted(result)}, f, indent=4, ensure_ascii=False)

    def custom_registryprefix(self):
        if str(self.options.oem) != "openUBMC":
            oem_path = os.path.join(self.source_folder, "oem", str(self.options.oem), "config.json")
            with open(oem_path, mode='rb') as fp:
                oem_data = json.load(fp)
            custom_message_path = os.path.join(self.source_folder, "messages/custom.json")
            with open(custom_message_path, mode='rb') as fp:
                custom_message_data = json.load(fp)
            custom_message_data["RegistryPrefix"] = oem_data["RegistryPrefix"]
            content = json.dumps(custom_message_data, separators=(',', ':'))
            with open(custom_message_path, mode='wb') as fd:
                fd.write(content.encode('utf-8'))

    def build(self):
        build_dir = self.source_folder
        self.check_mdb_path(build_dir)
        self.custom_registryprefix()
        self.parse_interface_path_mappings(build_dir)
        self.check_messages(build_dir)
        self.srv_file = os.path.join(os.getcwd(), "mds", "service.json")
        # 获取包名，默认取deps目录下的第一个目录名
        self.package_name = "mdb_interface"
        if self.package_name is None:
            self.package_name = comp.name
        proj = self.package_name

        bmcgo_dir = self.find_bmcgo_dir()
        tpl_dir = os.path.join(cwd, "lua_codegen")
        shutil.copytree(bmcgo_dir, tpl_dir, dirs_exist_ok=True)
        os.chdir(tpl_dir)

        lua_format = '/usr/bin/lua-format'
        if not os.path.exists(lua_format):
            raise Exception(f"Cannot found lua-format, please re-initialize your workspace.")

        self.run_command(f"LUA_FORMAT={lua_format} make PROJECT_NAME={proj} TPL_DIR={tpl_dir} BUILD_DIR={build_dir} messages")
        self.gen_messages_entry(build_dir)

        # 修改工作目录，添加上下文配置文件的权限设置命令
        context_conf_dir = os.path.join(build_dir, "dist")
        os.chdir(context_conf_dir)

        if self.settings.build_type == "Release":
            self.run("sed -i '$ a\opt/bmc/conf/context.json f 440 101 103' permissions.ini")
        else:
            self.run("sed -i '$ a\opt/bmc/conf/context.json f 444 0 0' permissions.ini")
