#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2026 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 json
import math
import re
import os
import stat
from tempfile import NamedTemporaryFile
from pathlib import Path
from typing import Iterable, List, Dict, Sequence
from collections import OrderedDict
from bmcgo.tasks.task import Task
from bmcgo.logger import Logger

log = Logger("combine_whitelist_json")


def unique_preserve_order(items: Iterable[str]) -> List[str]:
    seen = set()
    out = []
    for it in items:
        if it not in seen:
            seen.add(it)
            out.append(it)
    return out


def normalize_token(tok: str) -> str:
    if not isinstance(tok, str):
        tok = str(tok)
    return tok


def merge_command_json_files_by_list(
        task: Task,
        file_paths: Sequence[str | Path],
        out_file: str | Path = "merged.json",
        encoding: str = "utf-8",
        case_insensitive_sort: bool = True,
    ) -> Dict:
    """合并传入的 JSON 文件路径列表。
    - file_paths: 要合并的 JSON 文件路径列表（按列表顺序决定合并优先级/出现顺序）
    - out_file: 输出文件路径
    - case_insensitive_sort: 若排序，是否使用不区分大小写的排序
    返回字典包含合并结果和统计信息
    """
    collected_static: List[str] = []
    collected_dynamic: List[str] = []
    collected_safe_tokens: List[str] = []

    stats = {
        "files_total": 0,
        "files_skipped": 0,
        "static_total_before": 0,
        "dynamic_total_before": 0,
        "safe_tokens_total_before": 0
    }

    # 逐文件按传入顺序处理
    for fp in file_paths:
        stats["files_total"] += 1
        p = Path(fp)
        if not p.exists() or not p.is_file():
            log.info(f"skip {p}: not found or not a file")
            stats["files_skipped"] += 1
            continue
        with p.open("r", encoding=encoding) as f:
            data = json.load(f)

        # static_commands: 单文件内先去重并规范化
        sc = data.get("static_commands", [])
        if isinstance(sc, list):
            seen_local = set()
            sc_local = []
            for item in sc:
                if not isinstance(item, str):
                    continue
                tok = normalize_token(item)
                if tok not in seen_local:
                    seen_local.add(tok)
                    sc_local.append(tok)
            stats["static_total_before"] += len(sc_local)
            collected_static.extend(sc_local)
        else:
            log.info(f"warn {p}: 'static_commands' is not a list, skip")

        # dynamic_patterns
        dp = data.get("dynamic_patterns", [])
        if isinstance(dp, list):
            seen_local = set()
            dp_local = []
            for item in dp:
                if not isinstance(item, str):
                    continue
                tok = normalize_token(item)
                if tok not in seen_local:
                    seen_local.add(tok)
                    dp_local.append(tok)
            stats["dynamic_total_before"] += len(dp_local)
            collected_dynamic.extend(dp_local)
        else:
            log.info(f"warn {p}: 'dynamic_patterns' is not a list, skip")

        # safe_chars: 按空白拆分并规范化
        scs = data.get("safe_chars", "")
        if isinstance(scs, str):
            tokens = [t for t in scs.split() if t != ""]
            tokens_norm = [normalize_token(t) for t in tokens]
            stats["safe_tokens_total_before"] += len(tokens_norm)
            collected_safe_tokens.extend(tokens_norm)
        else:
            log.info(f"warn {p}: 'safe_chars' is not a string, skip")

    # 跨文件去重并保留首次出现顺序
    merged_static = unique_preserve_order(collected_static)
    merged_dynamic = unique_preserve_order(collected_dynamic)
    merged_safe_tokens = unique_preserve_order(collected_safe_tokens)
    # 校验动态命令是否有效
    [re.compile(cmd) for cmd in merged_dynamic]

    # 对结果排序
    key_func = (str.lower if case_insensitive_sort else None)
    merged_static.sort(key=key_func)
    merged_dynamic.sort(key=key_func)
    merged_safe_tokens.sort(key=key_func)

    merged_safe = " ".join(merged_safe_tokens)
    static_cmd_max_len = max([len(cmd) for cmd in merged_static] or [0])
    dynamic_cmd_max_len = max([len(cmd) for cmd in merged_dynamic] or [0])

    result = {
        "command": {
            "static_commands": merged_static,
            "dynamic_patterns": merged_dynamic,
            "safe_chars": merged_safe,
            "single_cmd_max_len": max(static_cmd_max_len, dynamic_cmd_max_len),
            "max_static_cmd_num": math.ceil(len(merged_static) / 100) * 100,
            "max_dynamic_cmd_num": math.ceil(len(merged_dynamic) / 100) * 100,
            "interrupt": True,
            "enable": True
        }
    }

    # 写出
    tempfile = NamedTemporaryFile()
    with os.fdopen(
        os.open(
            tempfile.name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, stat.S_IWUSR | stat.S_IRUSR
        ),
        "w", encoding="utf-8"
    ) as load_f:
        json.dump(result, load_f, ensure_ascii=False, indent=2)

    # 复制汇总结果
    task.run_command(f"cp -r {tempfile.name} {out_file}", sudo=True)
    task.run_command(f"chmod 755 {out_file}", sudo=True)


class CombineWhitelistJson(Task):
    '''
    用途: 支持产品定制差异化安全切面白名单配置文件
    '''
    def __init__(self, config, launch_control_path, subsys_path, apps_path, rtos_subsys_path, work_name=""):
        super(CombineWhitelistJson, self).__init__(config, work_name)
        self.launch_control_path = launch_control_path
        self.subsys_path = subsys_path
        self.apps_path = apps_path
        self.config = config
        self.rtos_subsys_path = rtos_subsys_path
        self.process_whitelist_config = {}
        self.app_process_dict = {}


    def load_launch_control(self, file_path):
        def walk_process(process_config, process_name):
            for _, group_config in enumerate(process_config):
                for _, component in enumerate(group_config['components']):
                    self.app_process_dict[component['name']] = process_name

        with open(file_path, 'r') as handle:
            data = json.load(handle, object_pairs_hook=OrderedDict)
            for process_name, process_config in data['processes'].items():
                self.app_process_dict[process_name] = process_name
                if 'groups' not in process_config:
                    continue
                walk_process(process_config['groups'], process_name)


    def append_process_whitelist_config(self, whitelist_name, process_name, whitelist_path):
        if whitelist_name not in self.process_whitelist_config:
            self.process_whitelist_config[whitelist_name] = {}
        if process_name not in self.process_whitelist_config[whitelist_name]:
            self.process_whitelist_config[whitelist_name][process_name] = []
        self.process_whitelist_config[whitelist_name][process_name].append(whitelist_path)


    def load_app_config(self, app_path, whitelist_name):
        app_root = Path(app_path)
        # 当前仅处理命令白名单
        app_command_whitelist_path = app_root / f"include/whitelist/{whitelist_name}"
        # 组件配置是否存在
        if not app_command_whitelist_path.is_file():
            return
        # 组件对应进程是否已有产品级配置,如果有则跳过组件白名单配置
        process_name = self.app_process_dict.get(app_root.name)
        if process_name is None:
            return
        process_whitelist_path = Path(f"{self.subsys_path}/{process_name}/whitelist/{whitelist_name}")
        if process_whitelist_path.is_file():
            return
        self.append_process_whitelist_config(whitelist_name, process_name, str(app_command_whitelist_path.absolute()))


    def load_apps_config(self, apps_path, whitelist_name):
        root = Path(apps_path)
        # 获取第一级子目录
        subdirs = [d for d in root.iterdir() if d.is_dir()]
        for app_path in subdirs:
            self.load_app_config(str(app_path.absolute()), whitelist_name)


    def load_process_base_config(self, whitelist_name):
        root = Path(self.subsys_path)
        for process_path in root.iterdir():
            if not process_path.is_dir():
                continue
            process_whitelist_path = process_path / f'whitelist/{whitelist_name}'
            if process_whitelist_path.is_file():
                continue
            base_whitelist_path = process_path / f'base_whitelist/{whitelist_name}'
            if not base_whitelist_path.is_file():
                continue
            self.append_process_whitelist_config(whitelist_name, process_path.name, str(base_whitelist_path.absolute()))


    def clean_process_base_config(self):
        root = Path(self.rtos_subsys_path)
        for process_path in root.iterdir():
            if not process_path.is_dir():
                continue
            base_whitelist_path = process_path / 'base_whitelist'
            self.run_command(f"rm -rf {str(base_whitelist_path.absolute())}", sudo=True)


    def gather_command_whitelist(self):
        if "CommandWhiteList.json" in self.process_whitelist_config:
            for process_name, process_configs in self.process_whitelist_config["CommandWhiteList.json"].items():
                # 如果目录不存在创建目录
                self.run_command(f"mkdir -p {self.rtos_subsys_path}/{process_name}/whitelist", sudo=True)
                merge_command_json_files_by_list(self, process_configs, f"{self.rtos_subsys_path}/{process_name}/whitelist/CommandWhiteList.json")


    def gather_process_whitelist(self, whitelist_name):
        if whitelist_name == "CommandWhiteList.json":
            self.gather_command_whitelist()
        elif whitelist_name == "CrossPathWhiteList.json":
            pass
        elif whitelist_name == "PermissionRiskWhiteList.json":
            pass
        elif whitelist_name == "SoftLinkWhiteList.json":
            pass
        elif whitelist_name == "UserWhiteList.json":
            pass
        else:
            self.log(f"unknown whitelist name:{whitelist_name}")


    def generate_whitelist(self):
        log.info("加载launch_control配置")
        self.load_launch_control(self.launch_control_path)
        whitelist_list = ["CommandWhiteList.json", "CrossPathWhiteList.json", "PermissionRiskWhiteList.json", "SoftLinkWhiteList.json", "UserWhiteList.json"]
        for _, whitelist_name in enumerate(whitelist_list):
            log.info(f"加载{whitelist_name}组件配置")
            self.load_apps_config(self.apps_path, whitelist_name)
            self.load_process_base_config(whitelist_name)
            self.gather_process_whitelist(whitelist_name)
        # 清理多余进程基础白名单配置
        self.clean_process_base_config()