#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""批量压测结果实时更新脚本(out_res/批量压测结果.xlsx)

每跑完一个接口的一个场景压测(预检/调试/全量各算一条记录),立即调用本脚本更新一次对应行。
openpyxl + 文件锁实现,多卡并行写安全(N 个 turing_sec 实例同时更新互不冲突)。

用法(与 AGENTS.md "批量多接口压测调度策略" 章节引用保持一致):
  python scripts/update_batch_results.py \
    --api aclnnAbs --scenario normal \
    --total 10000 --success 9987 --fail 13 \
    --fail_reason "OOM/shape过大 8条; 161001 dtype不支持 5条" \
    --has_bug 不是bug --has_aicore 否 --card 0 \
    --analyzed 否 --issue ""

  # 预检/调试记录使用带 _precheck 后缀的 scenario(如 normal_precheck / multithread_precheck / consistent_precheck)
  # 场景行已存在则更新(upsert),不存在则追加

表格列(8 个强制维度 + 4 个辅助/分析维度):
  1.接口名 2.测试场景 3.压测总条数 4.实际执行成功条数 5.实际执行失败条数
  6.执行失败原因 7.是否存在bug 8.是否存在aicore 9.执行卡号(辅) 10.更新时间(辅)
  11.是否分析完成 12.问题单

"是否存在bug"为三态列(强制规范): 是bug / 不是bug / 需要人工确认
  - 是bug:已确认问题(aicore_error 提单 / hash_mismatch>0 提单 / error 场景资料描述场景
    doc_based=true 的意外成功(错误码不符不判问题、不记录) / 修复循环耗尽等)
  - 不是bug:确认非问题(成功率达标且无 aicore_error / CPP 构造问题已修复 /
    资料描述场景外不校验(error 模式 doc_based=false 场景不做错误码校验)等)
  - 需要人工确认:暂无法自动判定(接口行为与资料描述不符待复核 / 根因存疑等)
"是否分析完成": 是/否(该行结果是否已完成人工/Agent 分析)
"问题单": 问题单号或链接(无则留空)

远程结果数据库(123.60.114.238:/home/30039325/fuzz_result)同步见 scripts/sync_result_server.py。

GitCode 仓实时同步(Alice_zhou/issue):本地表格每次更新成功后自动调用
scripts/sync_batch_results_git.py 将结果合并推送到仓内 `批量压测结果.xlsx`
(多机安全合并:行键=接口名+测试场景,本机行优先、他机行保留,禁止整表覆盖)——
不论压测任务由哪台机器拉起,仓内始终汇聚各机器的最新结果。
--no_git_sync 跳过本次同步;--git_sync 强制同步;全局开关见 config/issue_tracker.json
的 batch_result_git_sync(默认 true),环境变量 BATCH_RESULT_GIT_SYNC=0/1 可覆盖。
"""
import argparse
import contextlib
import os
import socket
import sys
import time

try:
    import openpyxl
except ImportError:
    print("[ERROR] openpyxl 未安装,请先执行: pip install openpyxl", file=sys.stderr)
    sys.exit(1)

DEFAULT_XLSX = os.path.join("out_res", "批量压测结果.xlsx")

HEADERS = [
    "接口名",
    "测试场景",
    "压测总条数",
    "实际执行成功条数",
    "实际执行失败条数",
    "执行失败原因",
    "是否存在bug",
    "是否存在aicore",
    "执行卡号",
    "更新时间",
    "是否分析完成",
    "问题单",
]

# 是否存在bug 三态规范值
BUG_STATES = ("是bug", "不是bug", "需要人工确认")
# 三态归一化映射(宽松输入 → 规范值)
BUG_STATE_ALIASES = {
    "是bug": "是bug", "是": "是bug", "yes": "是bug", "true": "是bug", "y": "是bug", "1": "是bug",
    "不是bug": "不是bug", "否": "不是bug", "no": "不是bug", "false": "不是bug", "n": "不是bug", "0": "不是bug",
    "需要人工确认": "需要人工确认", "人工确认": "需要人工确认", "待确认": "需要人工确认",
    "需人工确认": "需要人工确认", "unknown": "需要人工确认", "uncertain": "需要人工确认",
}

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")


class CrossPlatformLock:
    """跨平台文件锁(Linux: fcntl.flock / Windows: msvcrt.locking),带重试。"""

    def __init__(self, path, timeout=60, retries=50):
        self.path = path
        self.timeout = timeout
        self.retries = retries
        self.fd = None

    def __enter__(self):
        self.fd = open(self.path, "a+")
        last_err = None
        for i in range(self.retries):
            try:
                try:
                    import fcntl

                    fcntl.flock(self.fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
                    return self
                except ImportError:
                    import msvcrt

                    self.fd.seek(0)
                    msvcrt.locking(self.fd.fileno(), msvcrt.LK_NBLCK, 1)
                    return self
            except OSError as err:
                last_err = err
                time.sleep(min(0.1 * (i + 1), 1.0))
        raise TimeoutError(
            "获取结果表格锁超时({}s): {} {}".format(self.timeout, self.path, last_err)
        )

    def __exit__(self, exc_type, exc_val, exc_tb):
        with contextlib.suppress(Exception):
            try:
                import fcntl

                fcntl.flock(self.fd.fileno(), fcntl.LOCK_UN)
            except ImportError:
                import msvcrt

                self.fd.seek(0)
                msvcrt.locking(self.fd.fileno(), msvcrt.LK_UNLCK, 1)
        with contextlib.suppress(Exception):
            self.fd.close()
        return False


def normalize_int(value, default=0):
    try:
        return int(str(value).strip())
    except (TypeError, ValueError):
        return default


def load_or_create(xlsx_path):
    """加载或新建工作簿,返回 (workbook, worksheet)。"""
    if os.path.isfile(xlsx_path):
        wb = openpyxl.load_workbook(xlsx_path)
        ws = wb.active
        # 表头校验:不完整则重建表头(保留数据行)
        header_row = [str(c.value) if c.value is not None else "" for c in ws[1]] if ws.max_row >= 1 else []
        if header_row[: len(HEADERS)] != HEADERS:
            for col_idx, name in enumerate(HEADERS, start=1):
                ws.cell(row=1, column=col_idx, value=name)
    else:
        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "批量压测结果"
        for col_idx, name in enumerate(HEADERS, start=1):
            ws.cell(row=1, column=col_idx, value=name)
        # 简单列宽
        widths = [22, 20, 12, 16, 16, 50, 30, 40, 10, 20, 12, 28]
        for col_idx, width in enumerate(widths, start=1):
            ws.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = width
    return wb, ws


def find_row(ws, api, scenario):
    """按 (接口名, 测试场景) 查找已有行号(upsert 语义),不存在返回 None。"""
    for row in range(2, ws.max_row + 1):
        cell_api = ws.cell(row=row, column=1).value
        cell_scenario = ws.cell(row=row, column=2).value
        if cell_api is not None and cell_scenario is not None:
            if str(cell_api).strip() == api and str(cell_scenario).strip() == scenario:
                return row
    return None


def normalize_bug_state(value):
    """是否是bug 三态归一化:是bug / 不是bug / 需要人工确认(无法识别时默认需要人工确认)。

    支持复合值(三态+标准理由,如 "不是bug(环境问题)" / "是bug(aicore_error提单)"):
    前缀为规范三态时保留完整复合值(理由写入 xlsx 列),仅宽松输入(是/否/待确认等)做归一。
    """
    if value is None:
        return "需要人工确认"
    raw = str(value).strip().rstrip("。.!!")
    key = raw.lower()
    if key in BUG_STATE_ALIASES:
        return BUG_STATE_ALIASES[key]
    for state in BUG_STATES:
        if key.startswith(state):
            return raw  # 保留复合值中的理由标注(如 "不是bug(环境问题)")
    return "需要人工确认({})".format(raw) if raw else "需要人工确认"


def git_sync_after_update(xlsx_path, api, scenario, force=None):
    """本地表格更新后实时同步 GitCode 仓(Alice_zhou/issue,多机合并推送)。

    失败仅告警不阻塞本地更新(下次场景更新自动重试);force=None 时按
    环境变量 BATCH_RESULT_GIT_SYNC > config batch_result_git_sync 判定开关。
    """
    try:
        sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
        from sync_batch_results_git import DEFAULT_CONFIG, git_sync_enabled, sync_local_xlsx

        if force is None:
            enabled, reason = git_sync_enabled(DEFAULT_CONFIG)
        elif force:
            enabled, reason = True, "cli --git_sync"
        else:
            enabled, reason = False, "cli --no_git_sync"
        if not enabled:
            print("[GIT_SYNC] skip ({})".format(reason))
            return
        ok, url, detail = sync_local_xlsx(
            xlsx_path,
            commit_msg="results: update {}/{} from {}".format(api, scenario, socket.gethostname()))
        if ok:
            print("[GIT_SYNC] {}/{} 已实时同步至 GitCode 仓: {} ({})".format(
                api, scenario, url or "Alice_zhou/issue", detail))
        else:
            print("[GIT_SYNC] WARN 同步失败(不阻塞本地更新): {}".format(detail))
    except Exception as exc:
        print("[GIT_SYNC] WARN 同步异常(不阻塞本地更新): {}".format(exc))


def issue_stats_sync_after_update(xlsx_path, api, scenario):
    """批量压测结果更新后实时同步 问题统计/接口压测结果与提单汇总.xlsx(GitCode 仓 Alice_zhou/issue)。

    "每次压测结果依次填充即可,不需要另外建立表格"——按 (接口名, 压测日期) upsert 累积;
    失败仅告警不阻塞本地更新(下次场景更新/定时兜底自动重试);独立开关
    ISSUE_STATS_GIT_SYNC/config issue_stats_git_sync(默认 true)。"""
    try:
        from sync_issue_stats_git import issue_stats_enabled, sync_api_summary

        enabled, reason = issue_stats_enabled()
        if not enabled:
            print("[STATS_SYNC] skip ({})".format(reason))
            return
        ok, url, detail = sync_api_summary(xlsx_path)
        if ok and url:
            print("[STATS_SYNC] {}/{} 接口压测结果与提单汇总已实时填充 GitCode 仓: {} ({})".format(
                api, scenario, url, detail))
        elif ok:
            print("[STATS_SYNC] skip: {}".format(detail))
        else:
            print("[STATS_SYNC] WARN 同步失败(不阻塞本地更新): {}".format(detail))
    except Exception as exc:
        print("[STATS_SYNC] WARN 同步异常(不阻塞本地更新): {}".format(exc))


def main():
    parser = argparse.ArgumentParser(
        description="批量压测结果实时更新(out_res/批量压测结果.xlsx,多卡并行写安全)"
    )
    parser.add_argument("--api", required=True, help="aclnn 接口名,如 aclnnAbs")
    parser.add_argument("--scenario", required=True,
                        help="测试场景:normal/non_contig/multithread/consistent/error/sample"
                             "(预检/调试记录:normal_precheck/multithread_precheck/consistent_precheck 等)")
    parser.add_argument("--total", required=True, help="压测总条数(计划总条数)")
    parser.add_argument("--success", required=True, help="实际执行成功条数(aclnn 实际调用 ret==ACL_SUCCESS)")
    parser.add_argument("--fail", default=None,
                        help="实际执行失败条数(缺省时自动 = total - success,未执行起来的用例也计入失败)")
    parser.add_argument("--fail_reason", default="无", help="执行失败原因摘要(TOP 原因 + plog 错误码),无失败填 无")
    parser.add_argument("--has_bug", default="需要人工确认",
                        help="是否存在bug(三态,强制规范):是bug / 不是bug / 需要人工确认;"
                             "宽松输入自动归一(是→是bug,否→不是bug,待确认→需要人工确认)")
    parser.add_argument("--has_aicore", default="否", help="是否存在aicore:是/否,是时附 kernel_name 与命中文件")
    parser.add_argument("--card", default="", help="执行卡号 deviceId(多卡并行追溯用)")
    parser.add_argument("--analyzed", default="否", help="是否分析完成:是/否(该行结果是否已完成分析)")
    parser.add_argument("--issue", default="", help="问题单号或链接(无则留空)")
    parser.add_argument("--note", default="", help="备注(可选,将合并进执行失败原因列)")
    parser.add_argument("--xlsx", default=DEFAULT_XLSX, help="xlsx 路径,默认 out_res/批量压测结果.xlsx")
    parser.add_argument("--no_git_sync", action="store_true",
                        help="跳过本次 GitCode 仓(Alice_zhou/issue)实时同步")
    parser.add_argument("--git_sync", action="store_true",
                        help="强制执行 GitCode 仓实时同步(覆盖 config/环境变量开关)")
    args = parser.parse_args()

    api = str(args.api).strip()
    scenario = str(args.scenario).strip()
    total = normalize_int(args.total)
    success = normalize_int(args.success)
    fail = normalize_int(args.fail, default=-1)
    if fail < 0:
        fail = total - success

    fail_reason = str(args.fail_reason).strip() or "无"
    if args.note:
        fail_reason = "{} | note: {}".format(fail_reason, str(args.note).strip())

    values = [
        api,
        scenario,
        total,
        success,
        fail,
        fail_reason,
        normalize_bug_state(args.has_bug),
        str(args.has_aicore).strip() or "否",
        str(args.card).strip(),
        time.strftime("%Y-%m-%d %H:%M:%S"),
        "是" if str(args.analyzed).strip().lower() in ("是", "yes", "true", "y", "1") else "否",
        str(args.issue).strip(),
    ]

    xlsx_path = args.xlsx
    os.makedirs(os.path.dirname(os.path.abspath(xlsx_path)), exist_ok=True)
    lock_path = xlsx_path + ".lock"
    os.makedirs(os.path.dirname(os.path.abspath(lock_path)), exist_ok=True)

    with CrossPlatformLock(lock_path):
        wb, ws = load_or_create(xlsx_path)
        row = find_row(ws, api, scenario)
        action = "update"
        if row is None:
            row = ws.max_row + 1 if ws.max_row >= 1 else 2
            if ws.max_row == 1 and ws.cell(row=1, column=1).value is None:
                row = 1
            action = "append"
        for col_idx, value in enumerate(values, start=1):
            cell = ws.cell(row=row, column=col_idx, value=value)
            # 问题单列:issue 链接设为超链接(可直接点击跳转)
            if col_idx == 12 and value and str(value).startswith("http"):
                first_url = str(value).split("\n")[0].strip()
                if first_url.startswith("http"):
                    cell.hyperlink = first_url
        wb.save(xlsx_path)

    success_rate = (success * 100.0 / total) if total > 0 else 0.0
    print("[BATCH_RESULT] {} row={} api={} scenario={} total={} success={} fail={} rate={:.2f}% -> {}".format(
        action, row, api, scenario, total, success, fail, success_rate, xlsx_path))

    # 实时同步 GitCode 仓(Alice_zhou/issue,多机合并推送;失败不阻塞本地流程)
    if args.git_sync:
        git_sync_after_update(xlsx_path, api, scenario, force=True)
    elif args.no_git_sync:
        git_sync_after_update(xlsx_path, api, scenario, force=False)
    else:
        git_sync_after_update(xlsx_path, api, scenario, force=None)

    # 问题统计目录(强制):每次压测结果依次填充 接口压测结果与提单汇总.xlsx
    # (独立于批量结果 Git 同步开关,--no_git_sync 不影响本表填充;ISSUE_STATS_GIT_SYNC 可关)
    issue_stats_sync_after_update(xlsx_path, api, scenario)
    return 0


if __name__ == "__main__":
    sys.exit(main())