# -------------------------------------------------------------------------
#  This file is part of the MindStudio project.
# Copyright (c) 2025 Huawei Technologies Co.,Ltd.
#
# MindStudio 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 collections
import datetime
import json
import os
import shutil
from copy import copy
from typing import Optional

import pandas as pd
from openpyxl import load_workbook, Workbook
from openpyxl.utils.dataframe import dataframe_to_rows

from atk.configs.results_config import RunStatus, TaskResult
from atk.configs.base_config import TaskTypesSelect, NONE_RET
from atk.common.log import Logger
from atk.configs.report_config import CsvReport, BaseReportConfig
from atk.configs.standard_config import PerfStandardConfig, PassStandard
from atk.tasks.post_process.perf_compare_tool import PerfCompareExecutor
from atk.tasks.report.report_title.summary_report_title import SummaryReportTitleFactory
from atk.tasks.report.report_title.base_report_title import TitleType
from atk.tasks.report.report_title import summary_report_title
from atk.common.output_manager import OutputManager
from atk.common.utils import get_ttk_report_dir, chmod_output_path

logging = Logger().get_logger()


class SuccessInfo:
    def __init__(self):
        self.succ = set()
        self.fail = set()
        self.null = set()

    @property
    def length(self):
        return len(self.succ.union(self.fail).union(self.null))

    @property
    def succ_length(self):
        return len(self.succ)

    @property
    def fail_length(self):
        return len(self.fail)

    @property
    def null_length(self):
        return len(self.null)

    def add(self, id, succ):
        if succ == NONE_RET:
            func = self.null
            other_funcs = [self.succ, self.fail]
        elif succ:
            func = self.succ
            other_funcs = [self.null, self.fail]
        else:
            func = self.fail
            other_funcs = [self.null, self.succ]

        func.add(id)
        for other_fun in other_funcs:
            if id in other_fun:
                other_fun.remove(id)


class SummaryCsv(BaseReportConfig):
    SUCCESS_INFO = {
        "accuracy": {"key": "accuracy_pass"},
        "perf": {"key": "performance_e2e_result_node"},
        "perf_device": {"key": "performance_device_result_node"},
        "memory": {"key": "memory_result_node"},
    }

    def __init__(self, nodes_config):
        self.report_title_factory = None
        super(SummaryCsv, self).__init__(nodes_config)
        self.total = 0
        self.csv_reports = {}
        self.success_infos = {}
        self.e2e_perf_total = {}
        self.device_perf_total = {}
        self.perf_standard = None
        self.acc_pass_standard = PassStandard().get_acc_pass_ratio()
        self.mem_pass_standard = PassStandard().get_memory_pass_ratio()
        self.init()
        self.failed_number = set()

    def __str__(self):
        report_data = self.report_title_factory.get_report_data(TitleType.API_TITLE)
        data = report_data["data_en"]
        return str(json.dumps(data, ensure_ascii=False))

    @property
    def case_num(self):
        return len(self.csv_reports.keys())

    @property
    def success_num(self):
        return self.case_num - self.failed_num

    @property
    def failed_num(self):
        return len(self.failed_number)

    @staticmethod
    def get_avg_perf_ratio(node, perf_total):
        _input = perf_total.get(node.get_backend_name())
        data = _input.values()
        data = list(filter(lambda x: x is not None, data))
        if data:
            ratio = sum(data) / len(data)
            return "{:.4f}".format(ratio)
        else:
            return NONE_RET

    def update_standard(self, pass_standard: PassStandard):
        self.acc_pass_standard = pass_standard.get_acc_pass_ratio()
        self.mem_pass_standard = pass_standard.get_memory_pass_ratio()

    def init_report_title_factory(self):
        self.report_title_factory = SummaryReportTitleFactory(self)

    def init(self):
        for node in self.compare_nodes:
            backend_name = node.get_backend_name()
            for info, _ in self.SUCCESS_INFO.items():
                key = f"{backend_name}_{info}"
                self.success_infos[key] = SuccessInfo()
            self.e2e_perf_total[backend_name] = {}
            self.device_perf_total[backend_name] = {}

    def add(self, report: CsvReport):
        case_id = report.id
        self.csv_reports[case_id] = report
        if len(self.csv_reports) == 1:
            psd = report.task_result.get_pass_standard()
            self.update_standard(psd)
        main_node = self.nodes_config.get_main_node()
        if report.run_status != RunStatus.SUCCESS:
            self.add_success_info_false(case_id)
            self.failed_number.add(case_id)
            return
        if case_id in self.failed_number:
            self.failed_number.remove(case_id)
        for node in self.compare_nodes:
            backend_name = node.get_backend_name()
            for info, info_data in self.SUCCESS_INFO.items():
                key = f"{backend_name}_{info}"
                if key in self.success_infos:
                    self.success_infos[key].add(
                        case_id, report.get_value(info_data.get("key"), node)
                    )
            self.e2e_perf_total[backend_name][case_id] = report.get_value(
                "one_div_one_e2e_perf", node
            )
            self.device_perf_total[backend_name][case_id] = report.get_value(
                "one_div_one_device_perf", node
            )

    def add_success_info_false(self, case_id):
        main_node = self.nodes_config.get_main_node()
        for node in self.compare_nodes:
            # 失败用例只统计在精度里
            if node == main_node:
                continue
            backend_name = node.get_backend_name()
            for info, _ in self.SUCCESS_INFO.items():
                key = f"{backend_name}_{info}"
                if key in self.success_infos:
                    self.success_infos[key].add(case_id, False)

    def add_csv_json_failed(self, case_id):
        self.csv_reports[case_id] = {}
        self.add_success_info_false(case_id)

    def add_csv_json(self, csv_json):
        if self.perf_standard is None:
            perf_standard = PerfStandardConfig()
            perf_standard.update(**csv_json["perf_standard"])
            self.perf_standard = perf_standard
        self.add(csv_json)

    def acc_case_num(self, node, info):
        main_node = self.nodes_config.get_main_node()
        key = f"{main_node.get_backend_name()}_{info}"
        _input = self.success_infos[key]
        return _input.length

    def error_case_num(self, node, info):
        main_node = self.nodes_config.get_main_node()
        key = f"{main_node.get_backend_name()}_{info}"
        _input = self.success_infos[key]
        if _input.length == 0:
            return NONE_RET
        return _input.length - _input.null_length

    def fail_case_num(self, node, info):
        main_node = self.nodes_config.get_main_node()
        main_node_key = f"{main_node.get_backend_name()}_{info}"
        key = f"{node.get_backend_name()}_{info}"
        main_node_input = self.success_infos.get(main_node_key)
        _input = self.success_infos.get(key)
        return _input.length - main_node_input.length

    def acc_pass_num(self, node, info):
        main_node = self.nodes_config.get_main_node()
        main_node_key = f"{main_node.get_backend_name()}_{info}"
        key = f"{node.get_backend_name()}_{info}"
        main_node_input = self.success_infos.get(main_node_key)
        _input = self.success_infos.get(key)
        return _input.succ_length + _input.null_length - main_node_input.fail_length

    def pass_ratio(self, node, info):
        key = f"{node.get_backend_name()}_{info}"
        _input = self.success_infos.get(key)
        if _input.length == 0:
            return NONE_RET
        main_node = self.nodes_config.get_main_node()
        main_node_key = f"{main_node.get_backend_name()}_{info}"
        main_node_key_input = self.success_infos.get(main_node_key)
        pass_case = _input.succ_length + _input.null_length - main_node_key_input.fail_length
        pass_ratio = 100.0 * pass_case / main_node_key_input.length
        return round(pass_ratio, 4)

    def get_avg_perf_e2e_ratio(self, node):
        return SummaryCsv.get_avg_perf_ratio(node, self.e2e_perf_total)

    def get_avg_perf_device_ratio(self, node):
        return SummaryCsv.get_avg_perf_ratio(node, self.device_perf_total)

    def get_acc_pass_result(self, node) -> Optional[str]:
        _input = self.pass_ratio(node, "accuracy")
        if _input == NONE_RET or all([task not in node.task for task in TaskTypesSelect.ACCURACY_TASKS]):
            return NONE_RET
        return "Pass" if _input >= 100.0 * self.acc_pass_standard else "Failed"

    def get_acc_dc_pass_result(self, node):
        _input = self.pass_ratio(node, "accuracy")
        if _input == NONE_RET or all([task not in node.task for task in TaskTypesSelect.ACCURACY_DC_TASKS]):
            return "-"
        return "Pass" if _input >= 100.0 * self.acc_pass_standard else "Failed"

    def get_is_perf_pass(self, node, node_key):
        backend_name = node.get_backend_name()
        node_perf = self.success_infos.get(f"{backend_name}_perf")
        node_avg_perf_ratio = self.get_value(node_key, node)
        data = PerfCompareExecutor.compare_summary(
            node_perf, node_avg_perf_ratio, self.perf_standard
        )
        return data

    def get_is_memory_pass(self, node) -> Optional[str]:
        _input = self.get_value("memory_pass_ratio_node", node)
        if _input == NONE_RET:
            return NONE_RET
        return "Pass" if _input >= 100.0 * self.mem_pass_standard else "Failed"

    def get_mutil_perf_ratio(self, node):
        backend = node.backend
        _input = self.e2e_perf_total.get(node.get_backend_name())
        mutil_perf_ratio = ""
        perf_ratios = [0.25, 0.30, 0.80, 1.00]
        for ratio in perf_ratios:
            filtered_dict = {k: v for k, v in _input.items() if v and v >= ratio}
            num = (
                "{:.4f}".format(100.0 * len(filtered_dict) / len(_input))
                if len(filtered_dict)
                else "NA"
            )
            mutil_perf_ratio += f"{ratio:.2f}{backend}的占比: {num}\n"
        return mutil_perf_ratio.strip()


class CSVWriter:
    def __init__(self, nodes_config):
        self.all_cases = {}
        self.nodes_config = nodes_config
        self.compare_nodes = nodes_config.get_compare_nodes()
        self.summary_info = SummaryCsv(nodes_config)
        self.now_time = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
        self.output = OutputManager.get_dir_by_env("report")
        os.makedirs(self.output, exist_ok=True)
        self.api_name = None
        self.benchmark_path = None
        self.csv_path = None
        self.reports_path = None
        self.csv_report = None

    @staticmethod
    def get_column_index(sheet, column_name):
        """
        在 Excel 工作表中查找列名对应的列号(从1开始)。
        """
        for cell in sheet[1]:  # 遍历第一行(标题行)
            if cell.value == column_name:
                return cell.column  # 返回列号
        raise ValueError(f"can not find column:{column_name}")

    @staticmethod
    def merge_ttk_files(input_folder, merged_sheet):
        """
        合并指定文件夹中的所有 Excel 文件。
        第一个 Excel 文件完全复制,后续文件只复制非标题数据。
        """
        # 获取文件夹中所有 Excel 文件
        files = [f for f in os.listdir(input_folder) if f.endswith('.xlsx') or f.endswith('.xls')]
        if not files:
            return
        first_file = os.path.join(input_folder, files[0])
        first_workbook = load_workbook(first_file)
        first_sheet = first_workbook.active

        def set_cell_style(cell, new_cell):
            if cell.has_style:
                new_cell.font = copy(cell.font)
                new_cell.border = copy(cell.border)
                new_cell.fill = copy(cell.fill)
                new_cell.number_format = copy(cell.number_format)
                new_cell.protection = copy(cell.protection)
                new_cell.alignment = copy(cell.alignment)

        # 复制第一个文件的标题行格式
        for row in first_sheet.iter_rows():
            for cell in row:
                new_cell = merged_sheet.cell(row=cell.row, column=cell.column, value=cell.value)
                set_cell_style(cell, new_cell)

        merged_cells = first_sheet.merged_cells.ranges
        # 复制第一个文件的合并单元格
        for merged_cell in merged_cells:
            merged_sheet.merge_cells(str(merged_cell))

        title_row = 1
        for merged_cell in merged_cells:
            if merged_cell.min_col == 1 and merged_cell.min_row == 1:  # A1 单元格
                # 返回合并单元格的行跨度
                title_row = merged_cell.max_row - merged_cell.min_row + 1
        # 合并后续文件的数据(跳过标题行)
        for file in files[1:]:
            file_path = os.path.join(input_folder, file)
            workbook = load_workbook(file_path)
            sheet = workbook.active

            # 获取当前合并表的最大行数
            max_row = merged_sheet.max_row
            # 复制数据(跳过标题行)
            for row in sheet.iter_rows(min_row=title_row + 1):
                max_row = max_row + 1
                for cell in row:
                    new_cell = merged_sheet.cell(row=max_row, column=cell.column, value=cell.value)
                    set_cell_style(cell, new_cell)

    def set_api_name(self, api_name):
        self.api_name = api_name
        self.benchmark_path = os.path.join(
            self.output, f"{self.api_name}_benchmark_{self.now_time}.csv"
        )
        self.csv_path = os.path.join(
            self.output, f"{self.api_name}_statistic_{self.now_time}.csv"
        )
        self.reports_path = os.path.join(
            self.output, f"{self.api_name}_reports_{self.now_time}.xlsx"
        )

    def add_and_write_case(self, task_result: TaskResult):
        try:
            self.csv_report = CsvReport(task_result)
            if self.summary_info.perf_standard is None:
                self.summary_info.perf_standard = self.csv_report.perf_standard
            self.summary_info.add(self.csv_report)
            data = collections.OrderedDict()
            for en_title, zh_title in self.csv_report.get_print_name_list():
                data[zh_title] = getattr(self.csv_report, en_title)
            self.all_cases[self.csv_report.id] = data
        except Exception as e:
            logging.error(f'{task_result.case_config.name}_{task_result.case_config.id} add task report failed')
            logging.exception(e)

    def add_report(self, csv_report):
        try:
            if self.summary_info.perf_standard is None:
                self.summary_info.perf_standard = self.csv_report.perf_standard
            self.summary_info.add(csv_report)
            data = collections.OrderedDict()
            for en_title, zh_title in csv_report.get_print_name_list():
                data[zh_title] = getattr(csv_report, en_title)
            self.all_cases[csv_report.id] = data
        except Exception as e:
            logging.error(f'{csv_report.name}_{csv_report.id} add task report failed')
            logging.exception(e)

    def save_csv(self):
        if len(self.all_cases) == 0:
            logging.error('All cases is running failed.')
            return
        df_data = pd.DataFrame(self.all_cases).transpose()
        header = [row[-1] for row in self.csv_report.get_print_name_list()]
        df_data = df_data[header]
        df_data.to_csv(self.csv_path, index=False, header=True, encoding="utf_8_sig")
        logging.debug(f'save result csv success on {self.csv_path}')

    def save_benchmark(self, benchmark_ret_list: list):
        for benchmark_ret in benchmark_ret_list:
            df_data = pd.DataFrame(benchmark_ret, index=[0])
            if not os.path.exists(self.benchmark_path):
                df_data.to_csv(
                    self.benchmark_path,
                    index=False,
                    header=True,
                    encoding="utf_8_sig",
                )
            else:
                df_data.to_csv(self.benchmark_path, mode="a", index=False, header=False)

    def print_summary(self):
        report_title_type = summary_report_title.ReportTitlesType
        print_titles = {
            "acc_pass_result": {"key": report_title_type.ACC_PASS_RESULT_NODE},
            "is_acc_dc_pass": {"key": report_title_type.IS_ACC_DC_PASS_NODE},
            "is_e2e_perf_pass": {"key": report_title_type.IS_PERF_E2E_PASS_NODE},
            "is_device_perf_pass": {"key": report_title_type.IS_PERF_DEVICE_PASS_NODE},
            "is_device_memory_pass": {"key": report_title_type.IS_MEMORY_PASS_NODE},
        }
        data_str = ""
        for en_title, info in print_titles.items():
            sum_value = None
            for node in self.compare_nodes:
                value = self.summary_info.get_value(info.get("key"), node=node)
                if value is None:
                    continue
                if value == "Failed":
                    sum_value = "Failed"
                    break
                elif sum_value is None:
                    sum_value = "Pass"
            data_str += f"{en_title}:{sum_value}\t"
        success_len = self.summary_info.success_num
        failed_len = self.summary_info.failed_num
        logging.info(f"Total Task: {self.summary_info.case_num}, success {success_len}, failed {failed_len}")
        logging.info(f"Summary info: {data_str.strip()}.")

    def summary(self):
        self.save_csv()
        print_info = self.summary_info.get_print_table()
        table = print_info["table"]
        zh_title = print_info["zh_title"]
        data_list = print_info["data_list"]
        logging.info(f"\n{table}")
        self.print_summary()

        merged_workbook = Workbook()
        merged_workbook.remove(merged_workbook.active)  # 移除默认创建的工作表

        if os.path.exists(self.csv_path):
            # 读取csv保存成一个statistic sheet页
            df_statistic = pd.read_csv(self.csv_path)
            new_ws = merged_workbook.create_sheet(title="statistic")
            for r in dataframe_to_rows(df_statistic, index=False, header=True):
                new_ws.append(r)
            os.remove(self.csv_path)

        # 保存成一个summary sheet页
        summary_frame = pd.DataFrame(data_list, columns=zh_title)
        new_ws = merged_workbook.create_sheet(title="summary")
        for r in dataframe_to_rows(summary_frame, index=False, header=True):
            new_ws.append(r)

        # 保存执行失败的case页
        failed_cases_ws = merged_workbook.create_sheet(title="failed cases")
        failed_pd = df_statistic[df_statistic['运行结果'] == 'FAILED']
        for r in dataframe_to_rows(failed_pd, index=False, header=True):
            failed_cases_ws.append(r)

        # 保存精度比对失败的case页
        accuracy_false_ws = merged_workbook.create_sheet(title="accuracy false cases")
        matched_cols = [col for col in df_statistic.columns if "精度通过" in col]
        if matched_cols:
            target_col = matched_cols[-1]
            acc_false_pd = df_statistic[(df_statistic['运行结果'] == 'SUCCESS') & (df_statistic[target_col] is False)]
            for r in dataframe_to_rows(acc_false_pd, index=False, header=True):
                accuracy_false_ws.append(r)

        task_dir_name = OutputManager.get_task_output_file_name_by_env()
        ttk_report_dir = get_ttk_report_dir(self.output, task_dir_name, create=False)
        if os.path.exists(ttk_report_dir):
            md5_sheet = None
            for folder in os.listdir(ttk_report_dir):
                folder_dir = os.path.join(ttk_report_dir, folder)
                merged_sheet = merged_workbook.create_sheet(title=folder)
                self.merge_ttk_files(folder_dir, merged_sheet)
                if folder == "BinaryCompare":
                    md5_sheet = merged_sheet
            shutil.rmtree(ttk_report_dir)
            if md5_sheet:
                self.merge_benchmark_files_to_ttk(md5_sheet)
            else:
                self.merge_benchmark_files(merged_workbook)
        else:
            self.merge_benchmark_files(merged_workbook)
        merged_workbook.save(filename=self.reports_path)

        chmod_output_path(os.path.join(self.reports_path))
        logging.info(f"save result excel file: {os.path.join(self.reports_path)}")

    def merge_benchmark_files(self, merged_workbook):
        if self.benchmark_path and os.path.exists(self.benchmark_path):
            logging.debug("start merge benchmark_file")
            merged_sheet = merged_workbook.create_sheet(title="BinaryCompare")
            df_benchmark = pd.read_csv(self.benchmark_path)
            for r in dataframe_to_rows(df_benchmark, index=False, header=True):
                merged_sheet.append(r)
            os.remove(self.benchmark_path)

    def merge_benchmark_files_to_ttk(self, sheet):
        if self.benchmark_path and os.path.exists(self.benchmark_path):
            logging.debug("start merge benchmark_file to ttk")
            csv_column_map = {
                "用例ID": "用例",
                "用例名称": "算子名称",
                "错误信息": "message",
                "WARNING状态": "精度结果",
            }
            csv_data = pd.read_csv(self.benchmark_path)
            # 动态获取目标列的索引
            excel_column_indices = {}
            for csv_column, excel_column_name in csv_column_map.items():
                excel_column_indices[csv_column] = self.get_column_index(sheet, excel_column_name)


            max_row = sheet.max_row
            for index, row in csv_data.iterrows():
                new_row = max_row + index + 1
                for csv_column, excel_column_index in excel_column_indices.items():
                    cell = sheet.cell(row=new_row, column=excel_column_index)
                    cell.value = row[csv_column]
            os.remove(self.benchmark_path)