#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""问题统计目录同步至 GitCode 仓(Alice_zhou/issue,强制)

功能定位:在 GitCode 仓 Alice_zhou/issue 下维护 **问题统计/** 目录,内含两个 Excel 在线统计表,
压测/提单全流程实时填充(同一张表 upsert 累积,不按日期另建表格):

  1. 问题统计/接口压测结果与提单汇总.xlsx
     列:序号 | 接口名 | 对应主算子名 | sample | normal | non_contig | multithread | consistent |
         error | 总体结论 | 提单情况 | 备注 | 压测日期 | 压测版本
     - 各场景列:成功率xx%(error=已覆盖),无数据为 —
     - 总体结论(强制三态+着色):**fail(红色)**=存在 是bug(版本bug)记录;
       **pass(绿色)**=全部已执行场景成功率≥90% 且无版本bug;**待复核(黄色)**=存在场景成功率<90% 且无版本bug
     - 提单情况:该接口问题单列 issue 链接数(已提单N个/待提单/无)
     - 行键 = 接口名 + 压测日期(同接口重跑按日期新行,依次填充;同日重跑 upsert 覆盖)
  2. 问题统计/issue汇总.xlsx
     列:序号 | aclnn接口名 | 对应主算子名 | 问题 | issue链接 | md资料链接 | 压测日期
     - 数据源:out_res/issues/{日期}/daily_issues.json(主源)→ filed.json+summary.md(历史回退)
     - 行键 = aclnn接口名 + issue链接(同一 issue 多场景归并只占一行)
     - 对应主算子名:find_owner_repo.resolve_api 定位;md资料链接:本地 ops-* 仓 docs/aclnn*.md
       (排除 experimental)→ https://gitcode.com/cann/{repo}/blob/{branch}/{path};两者本地缓存(.meta_cache.json)

多机安全合并(同 sync_batch_results_git.py 机制,不丢任何机器的数据):
  - 推送前先 GET 远端 xlsx → 以远端为基底,将本机行 upsert 合并(键冲突时本机行优先——本机行是最新结果)
  - 远端存在而本机没有的行(他机写入)原样保留,禁止整表覆盖
  - PUT 携带远端 sha(乐观锁),并发冲突(409/sha 失配)自动重拉远端重新合并重试(最多 --retries 次)
  - 远端不存在时 POST 创建;合并完成后 序号 列全表重排、总体结论 着色按值重新应用

触发链路(自动,无需手动):
  - update_batch_results.py:每场景压测结果更新后实时推送 接口压测结果与提单汇总("每次压测结果依次填充")
  - file_bug_issues.py:每轮提单后实时推送 issue汇总 + 刷新 接口压测结果与提单汇总(提单情况列)
  - scheduled_update.py:每日 4 轮整点兜底补推两个表(实时同步失败/漏推的兜底)

API(已实测验证,同 scripts/sync_batch_results_git.py):GET/POST/PUT contents + PRIVATE-TOKEN。

用法:
  python scripts/sync_issue_stats_git.py                     # 生成/刷新本地两表并推送
  python scripts/sync_issue_stats_git.py --api-summary-only  # 仅 接口压测结果与提单汇总
  python scripts/sync_issue_stats_git.py --issue-summary-only# 仅 issue汇总
  python scripts/sync_issue_stats_git.py --no-push           # 仅本地生成不推送
  python scripts/sync_issue_stats_git.py --local-xlsx out_res/批量压测结果.xlsx [--json]

开关:环境变量 ISSUE_STATS_GIT_SYNC=0/1 > config issue_stats_git_sync(默认 true)
压测版本:环境变量 FUZZ_CANN_VERSION/CANN_VERSION > CANN version 文件 > "未知"
"""
import argparse
import base64
import datetime
import glob
import io
import json
import os
import re
import socket
import subprocess
import sys
import time

try:
    import openpyxl
    from openpyxl.styles import Font, PatternFill
except ImportError:
    openpyxl = None

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sync_batch_results_git import GitCodeContentsClient  # noqa: E402  复用认证与仓文件操作客户端
from sync_daily_issues_git import collect_daily_rows  # noqa: E402  当日已提 issue 行收集(含 filed 回退)

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

DEFAULT_CONFIG = "config/issue_tracker.json"
DEFAULT_LOCAL_XLSX = os.path.join("out_res", "批量压测结果.xlsx")
STATS_DIR_LOCAL = os.path.join("out_res", "问题统计")
META_CACHE_PATH = os.path.join(STATS_DIR_LOCAL, ".meta_cache.json")
DEFAULT_REPO_DIR = "问题统计"
API_SUMMARY_NAME = "接口压测结果与提单汇总.xlsx"
ISSUE_SUMMARY_NAME = "issue汇总.xlsx"
MAX_FILE_BYTES = 50 * 1024 * 1024
GATE_SCENARIOS = ("sample", "normal", "non_contig", "multithread", "consistent")  # 有成功率门禁的场景
ALL_SCENARIOS = GATE_SCENARIOS + ("error",)
PASS_RATE = 90.0

API_HEADERS = ["序号", "接口名", "对应主算子名", "sample", "normal", "non_contig",
               "multithread", "consistent", "error", "总体结论", "提单情况", "备注",
               "压测日期", "压测版本"]
ISSUE_HEADERS = ["序号", "aclnn接口名", "对应主算子名", "问题", "issue链接", "md资料链接", "压测日期"]

# 总体结论三态着色(Excel 内置 好/中/差 三色样式)
FILL_STYLES = {
    "pass": ("C6EFCE", "006100"),     # 绿色:全部场景≥90% 且无版本bug
    "待复核": ("FFEB9C", "9C6500"),   # 黄色:存在场景<90% 且无版本bug
    "fail": ("FFC7CE", "9C0006"),     # 红色:存在版本bug
}


def log(tag, msg):
    print("[{}] {}".format(tag, msg))


def _cell_str(value):
    return str(value).strip() if value is not None else ""


def _today():
    return datetime.date.today().strftime("%Y-%m-%d")


# ---------------- 压测版本 / 算子名 / md资料链接(带本地缓存) ----------------

def get_cann_version():
    """压测版本:环境变量 FUZZ_CANN_VERSION/CANN_VERSION > CANN version 文件 > 未知。"""
    for env in ("FUZZ_CANN_VERSION", "CANN_VERSION"):
        v = os.environ.get(env, "").strip()
        if v:
            return v
    home = os.environ.get("ASCEND_HOME_PATH", "").strip()
    candidates = []
    if home:
        candidates += [os.path.join(home, "version.cfg"), os.path.join(home, "version.info"),
                       os.path.normpath(os.path.join(home, "..", "version.cfg"))]
    candidates += ["/usr/local/Ascend/ascend-toolkit/latest/version.cfg",
                   "/usr/local/Ascend/ascend-toolkit/latest/version.info",
                   "/usr/local/Ascend/ascend-toolkit/latest/x86_64-linux/version.cfg"]
    for path in candidates:
        try:
            with open(path, encoding="utf-8", errors="replace") as fh:
                for line in fh:
                    line = line.strip()
                    m = re.match(r"^Version\s*=\s*(.+)$", line, re.I)
                    if m:
                        return m.group(1).strip()
                    if line and "=" not in line and not line.startswith("#"):
                        return line
        except Exception:
            continue
    return "未知"


def _load_meta_cache():
    if os.path.isfile(META_CACHE_PATH):
        try:
            data = json.load(open(META_CACHE_PATH, encoding="utf-8-sig"))
            if isinstance(data, dict):
                return data
        except ValueError:
            pass
    return {}


def _save_meta_cache(cache):
    try:
        os.makedirs(STATS_DIR_LOCAL, exist_ok=True)
        json.dump(cache, open(META_CACHE_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
    except Exception:
        pass


def resolve_op_name(api):
    """对应主算子名(find_owner_repo 定位,本地缓存;失败返回 未定位)。"""
    cache = _load_meta_cache()
    entry = cache.get(api) or {}
    if "op" in entry:
        return entry["op"]
    op = "未定位"
    try:
        from find_owner_repo import resolve_api
        info = resolve_api(api)
        op = str(info.get("op") or "未定位")
    except Exception:
        pass
    entry["op"] = op
    cache[api] = entry
    _save_meta_cache(cache)
    return op


def find_md_link(api):
    """md资料链接:本地 ops-* 仓 docs/aclnn{api}*.md(排除 experimental)→ GitCode 公网链接。

    找不到(本地无 ops-* 仓/无该接口 md)返回 "未找到"。结果本地缓存。"""
    cache = _load_meta_cache()
    entry = cache.get(api) or {}
    if "md" in entry:
        return entry["md"]
    md_link = "未找到"
    try:
        repos = sorted(glob.glob("ops-*"))
        hits = []
        for repo in repos:
            if not os.path.isdir(repo):
                continue
            for path in glob.glob(os.path.join(repo, "**", "docs", "*.md"), recursive=True):
                low = path.replace("\\", "/").lower()
                if "/experimental/" in low:
                    continue
                base = os.path.splitext(os.path.basename(path))[0]
                parts = base.split("&")
                if api in parts:
                    hits.append((repo, path))
                    break
            if hits:
                break  # 优先第一个命中仓(ops-cv < ops-math < ops-nn < ops-transformer 字典序)
        if hits:
            repo, path = hits[0]
            branch = "master"
            try:
                r = subprocess.run(["git", "-C", repo, "rev-parse", "--abbrev-ref", "HEAD"],
                                   capture_output=True, text=True, timeout=30)
                if r.returncode == 0 and r.stdout.strip():
                    branch = r.stdout.strip()
            except Exception:
                pass
            rel = os.path.relpath(path, repo).replace("\\", "/")
            md_link = "https://gitcode.com/cann/{}/blob/{}/{}".format(repo, branch, rel)
    except Exception:
        pass
    entry["md"] = md_link
    cache[api] = entry
    _save_meta_cache(cache)
    return md_link


# ---------------- 表1:接口压测结果与提单汇总 ----------------

def _scenario_base(scenario):
    """场景归一:normal_precheck/consistent_precheck 等预检记录归并到基场景(取全量记录优先)。"""
    s = str(scenario or "").strip()
    for suf in ("_precheck", "_debug"):
        if s.endswith(suf):
            return s[: -len(suf)], False
    return s, True


def collect_api_rows(local_xlsx):
    """本地批量压测结果 → 接口汇总行(按 接口名+压测日期 聚合)。

    返回 rows: [{api, op, scenes{scenario: 率文本}, rates{scenario: pct}, has_bug, issue_links,
    remark, date}]。全量记录优先于预检记录(同场景预检仅在全量缺失时作临时值)。"""
    if openpyxl is None or not os.path.isfile(local_xlsx):
        return []
    wb = openpyxl.load_workbook(local_xlsx, read_only=True)
    ws = wb.active
    rows_raw = list(ws.iter_rows(values_only=True))
    wb.close()
    if not rows_raw:
        return []
    header = [_cell_str(c) for c in rows_raw[0]]
    idx = {h: i for i, h in enumerate(header)}
    if "接口名" not in idx:
        return []

    agg = {}  # (api, date) → {scenes: {base: (rate_text, is_full)}, ...}
    for row in rows_raw[1:]:
        if not row or not any(_cell_str(c) for c in row if c is not None):
            continue

        def col(name, default=""):
            i = idx.get(name)
            return _cell_str(row[i]) if i is not None and i < len(row) and row[i] is not None else default

        api = col("接口名")
        if not api:
            continue
        upd = col("更新时间")
        m = re.match(r"(\d{4}-\d{2}-\d{2})", upd)
        date = m.group(1) if m else _today()
        base, is_full = _scenario_base(col("测试场景"))
        if base not in ALL_SCENARIOS:
            continue
        total = col("压测总条数")
        success = col("实际执行成功条数")
        try:
            t, s = int(float(total or 0)), int(float(success or 0))
        except ValueError:
            t, s = 0, 0
        key = (api, date)
        rec = agg.setdefault(key, {"scenes": {}, "bug": "", "issues": set(), "fail_reason": ""})
        if t > 0:
            pct = round(s * 100.0 / t, 1)
            text = "已覆盖" if base == "error" else "成功率{}%".format(int(pct) if pct == int(pct) else pct)
        else:
            pct = None
            text = "已覆盖" if base == "error" else "—"
        # 全量记录优先;预检记录仅在无全量记录时作为临时值
        prev = rec["scenes"].get(base)
        if prev is None or (is_full and not prev[2]):
            rec["scenes"][base] = (text, pct, is_full)
        bug = col("是否存在bug")
        if bug.startswith("是bug") and not rec["bug"]:
            rec["bug"] = bug
        if not rec["fail_reason"]:
            reason = col("执行失败原因")
            if reason and reason != "无":
                rec["fail_reason"] = reason
        for url in re.split(r"[\n;;]", col("问题单")):
            url = url.strip()
            if url.startswith("http"):
                rec["issues"].add(url)

    out = []
    for (api, date), rec in agg.items():
        rates = {sc: v[1] for sc, v in rec["scenes"].items() if v[1] is not None and sc != "error"}
        if rec["bug"]:
            verdict = "fail"
        elif rates and any(pct < PASS_RATE for pct in rates.values()):
            verdict = "待复核"
        elif rates:
            verdict = "pass"
        else:
            verdict = "—"
        if rec["issues"]:
            filed = "已提单{}个".format(len(rec["issues"]))
        elif rec["bug"]:
            filed = "待提单"
        else:
            filed = "无"
        remark = rec["bug"] or rec["fail_reason"]
        out.append({"api": api, "date": date, "scenes": {sc: rec["scenes"].get(sc, ("—", None, True))[0]
                                                          for sc in ALL_SCENARIOS},
                    "verdict": verdict, "filed": filed, "remark": remark[:80],
                    "issue_links": sorted(rec["issues"])})
    out.sort(key=lambda r: (r["date"], r["api"]))
    return out


def _apply_verdict_style(ws):
    """按 总体结论 列值重新应用三态着色(pass绿/待复核黄/fail红)与超链接。"""
    if openpyxl is None:
        return
    header = [_cell_str(c.value) for c in ws[1]] if ws.max_row >= 1 else []
    if "总体结论" not in header:
        return
    vcol = header.index("总体结论") + 1
    for row in range(2, ws.max_row + 1):
        verdict = _cell_str(ws.cell(row=row, column=vcol).value)
        style = FILL_STYLES.get(verdict)
        cell = ws.cell(row=row, column=vcol)
        if style:
            fill_color, font_color = style
            cell.fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid")
            cell.font = Font(color=font_color, bold=True)
        else:
            cell.fill = PatternFill(fill_type=None)
            cell.font = Font()


def build_api_summary_workbook(rows, out_path, cann_version):
    """生成本地 接口压测结果与提单汇总.xlsx(行序=日期+接口名,三态着色)。"""
    if openpyxl is None:
        raise RuntimeError("openpyxl 未安装(pip install openpyxl)")
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    op_cache = {}
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "接口压测结果与提单汇总"
    ws.append(API_HEADERS)
    for i, r in enumerate(rows, 1):
        op = op_cache.get(r["api"])
        if op is None:
            op = op_cache[r["api"]] = resolve_op_name(r["api"])
        ws.append([i, r["api"], op] + [r["scenes"].get(sc, "—") for sc in ALL_SCENARIOS]
                  + [r["verdict"], r["filed"], r["remark"], r["date"], cann_version])
    widths = [6, 26, 22, 10, 12, 13, 13, 13, 10, 10, 12, 40, 12, 16]
    for col_idx, width in enumerate(widths, start=1):
        ws.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = width
    _apply_verdict_style(ws)
    wb.save(out_path)
    return out_path


# ---------------- 表2:issue汇总 ----------------

def collect_issue_rows():
    """全部 out_res/issues/{日期}/ 已提 issue 行(daily_issues.json 主源 → filed.json+summary.md 回退)。

    返回 [{api, problem, issue, date}],按 (api, issue) 去重。"""
    rows, seen = [], set()
    issues_root = os.path.join("out_res", "issues")
    if not os.path.isdir(issues_root):
        return rows
    for date_str in sorted(os.listdir(issues_root)):
        day_dir = os.path.join(issues_root, date_str)
        if not os.path.isdir(day_dir) or not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
            continue
        for r in collect_daily_rows(date_str):
            key = (r["api"], r["issue"])
            if key in seen:
                continue
            seen.add(key)
            rows.append({"api": r["api"], "problem": r["problem"], "issue": r["issue"],
                         "date": date_str})
    rows.sort(key=lambda r: (r["date"], r["api"]))
    return rows


def build_issue_summary_workbook(rows, out_path):
    """生成本地 issue汇总.xlsx(序号/aclnn接口名/对应主算子名/问题/issue链接/md资料链接/压测日期)。"""
    if openpyxl is None:
        raise RuntimeError("openpyxl 未安装(pip install openpyxl)")
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "issue汇总"
    ws.append(ISSUE_HEADERS)
    for i, r in enumerate(rows, 1):
        ws.append([i, r["api"], resolve_op_name(r["api"]), r["problem"], r["issue"],
                   find_md_link(r["api"]), r["date"]])
    widths = [6, 26, 22, 60, 55, 55, 12]
    for col_idx, width in enumerate(widths, start=1):
        ws.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = width
    header = ISSUE_HEADERS
    for row in range(2, ws.max_row + 1):
        for col_idx, name in ((header.index("issue链接") + 1, "issue"), (header.index("md资料链接") + 1, "md")):
            cell = ws.cell(row=row, column=col_idx)
            if _cell_str(cell.value).startswith("http"):
                cell.hyperlink = _cell_str(cell.value)
    wb.save(out_path)
    return out_path


# ---------------- 多机合并推送(同 sync_batch_results_git 机制) ----------------

def _merge_rows(remote_bytes, local_xlsx, headers, key_cols, style_fn=None):
    """以远端 xlsx 为基底合并本机行(键冲突本机优先,他机行保留),返回 (合并字节, merged, appended)。

    key_cols: 行键列名列表(如 ["接口名", "压测日期"]);合并后 序号 列全表重排;
    style_fn: 合并完成后对全表重新应用样式(如三态着色)。远端解析失败抛异常(禁止整表覆盖)。"""
    if openpyxl is None:
        raise RuntimeError("openpyxl 未安装,无法合并(pip install openpyxl)")
    remote_wb = openpyxl.load_workbook(io.BytesIO(remote_bytes))
    remote_ws = remote_wb.active
    local_wb = openpyxl.load_workbook(local_xlsx)
    local_ws = local_wb.active

    if remote_ws.max_row < 1 or _cell_str(remote_ws.cell(row=1, column=2).value) != headers[1]:
        if remote_ws.max_row >= 1 and _cell_str(remote_ws.cell(row=1, column=2).value) == "":
            header_row = 1
        else:
            header_row = remote_ws.max_row + 1
        for col_idx, name in enumerate(headers, start=1):
            remote_ws.cell(row=header_row, column=col_idx, value=name)

    remote_header = [_cell_str(c.value) for c in remote_ws[1]]
    key_idx = [remote_header.index(k) + 1 for k in key_cols]
    url_cols = [i + 1 for i, h in enumerate(remote_header) if h in ("issue链接", "md资料链接", "问题单")]

    remote_map = {}
    for row in range(2, remote_ws.max_row + 1):
        key = tuple(_cell_str(remote_ws.cell(row=row, column=c).value) for c in key_idx)
        if all(key):
            remote_map[key] = row

    merged, appended = 0, 0
    for row in range(2, local_ws.max_row + 1):
        key = tuple(_cell_str(local_ws.cell(row=row, column=c).value) for c in key_idx)
        if not all(key):
            continue
        if key in remote_map:
            target = remote_map[key]
            merged += 1
        else:
            target = remote_ws.max_row + 1
            appended += 1
        for col_idx in range(1, len(remote_header) + 1):
            value = local_ws.cell(row=row, column=col_idx).value
            cell = remote_ws.cell(row=target, column=col_idx, value=value)
            if col_idx in url_cols and _cell_str(value).startswith("http"):
                cell.hyperlink = _cell_str(value)

    # 序号 列全表重排(远端+本机合并后统一顺序)
    if "序号" in remote_header:
        seq_col = remote_header.index("序号") + 1
        for n, row in enumerate(range(2, remote_ws.max_row + 1), 1):
            remote_ws.cell(row=row, column=seq_col, value=n)
    if style_fn is not None:
        style_fn(remote_ws)
    buf = io.BytesIO()
    remote_wb.save(buf)
    return buf.getvalue(), merged, appended


def issue_stats_enabled(config_path=DEFAULT_CONFIG):
    """同步开关:环境变量 ISSUE_STATS_GIT_SYNC > config issue_stats_git_sync(默认 true)。"""
    env = os.environ.get("ISSUE_STATS_GIT_SYNC", "").strip()
    if env:
        return env not in ("0", "false", "off", "no"), "env ISSUE_STATS_GIT_SYNC={}".format(env)
    if os.path.isfile(config_path):
        try:
            cfg = json.load(open(config_path, encoding="utf-8-sig"))
            enabled = cfg.get("issue_stats_git_sync", True)
            return bool(enabled), "config issue_stats_git_sync={}".format(enabled)
        except Exception:
            return True, "config 解析失败,按默认启用"
    return True, "默认启用(无配置文件)"


def _push_with_merge(local_xlsx, repo_path, config_path, headers, key_cols,
                     commit_msg, retries=3, style_fn=None):
    """本地 xlsx 合并推送至 GitCode 仓(sha 乐观锁 + 冲突重试)。返回 (ok, url, detail)。"""
    if openpyxl is None:
        return False, "", "openpyxl 未安装"
    if not os.path.isfile(local_xlsx):
        return False, "", "本地文件不存在: {}".format(local_xlsx)
    if os.path.getsize(local_xlsx) > MAX_FILE_BYTES:
        return False, "", "文件超过 {}MB 上限".format(MAX_FILE_BYTES // (1024 * 1024))
    try:
        client = GitCodeContentsClient(config_path)
    except Exception as exc:
        return False, "", "配置加载失败: {}".format(exc)
    last_err = ""
    for attempt in range(1, retries + 1):
        code, info = client.get_file(repo_path)
        if code == 200:
            sha = info.get("sha", "")
            content_b64 = info.get("content", "") or ""
            remote_bytes = base64.b64decode(content_b64) if content_b64 else b""
            try:
                merged_bytes, merged, appended = _merge_rows(remote_bytes, local_xlsx, headers,
                                                             key_cols, style_fn)
            except Exception as exc:
                return False, "", "远端文件合并失败(禁止覆盖远端,已中止): {}".format(exc)
            payload_b64 = base64.b64encode(merged_bytes).decode("ascii")
            code2, resp = client.update_file(repo_path, payload_b64, commit_msg, sha)
            if code2 in (200, 201):
                url = info.get("html_url", "") or info.get("download_url", "")
                return True, url, "merge(remote+local) merged={} appended={} host={}".format(
                    merged, appended, socket.gethostname())
            last_err = "PUT 失败 [{}]: {}".format(code2, json.dumps(resp, ensure_ascii=False)[:200])
            if code2 in (409, 400, 422):
                time.sleep(min(1.0 * attempt, 3.0))
                continue
        elif code == 404:
            payload_b64 = base64.b64encode(open(local_xlsx, "rb").read()).decode("ascii")
            code2, resp = client.create_file(repo_path, payload_b64, commit_msg)
            if code2 in (200, 201):
                code3, info2 = client.get_file(repo_path)
                url = (info2.get("html_url", "") or info2.get("download_url", "")) if code3 == 200 else ""
                return True, url, "create(new) host={}".format(socket.gethostname())
            last_err = "POST 失败 [{}]: {}".format(code2, json.dumps(resp, ensure_ascii=False)[:200])
            time.sleep(min(1.0 * attempt, 3.0))
        else:
            last_err = "GET 失败 [{}]: {}".format(code, json.dumps(info, ensure_ascii=False)[:200])
            time.sleep(min(1.0 * attempt, 3.0))
    return False, "", "重试 {} 次仍失败: {}".format(retries, last_err)


# ---------------- 对外入口(供 update_batch_results / file_bug_issues / scheduled_update 调用) ----------------

def sync_api_summary(local_xlsx=DEFAULT_LOCAL_XLSX, config_path=DEFAULT_CONFIG,
                     repo_dir=None, push=True, retries=3):
    """生成/刷新并推送 接口压测结果与提单汇总.xlsx。返回 (ok, url, detail)。"""
    rows = collect_api_rows(local_xlsx)
    if not rows:
        return True, "", "本地结果表无数据({}),跳过推送".format(local_xlsx)
    local_out = os.path.join(STATS_DIR_LOCAL, API_SUMMARY_NAME)
    cann_version = get_cann_version()
    build_api_summary_workbook(rows, local_out, cann_version)
    if not push:
        return True, local_out, "仅本地生成({} 行): {}".format(len(rows), local_out)
    try:
        cfg = json.load(open(config_path, encoding="utf-8-sig"))
    except Exception:
        cfg = {}
    repo_dir = repo_dir or cfg.get("issue_stats_repo_dir") or DEFAULT_REPO_DIR
    repo_path = "{}/{}".format(repo_dir, API_SUMMARY_NAME)
    return _push_with_merge(
        local_out, repo_path, config_path, API_HEADERS, ["接口名", "压测日期"],
        "stats: api summary {} rows from {}".format(len(rows), socket.gethostname()),
        retries, style_fn=_apply_verdict_style)


def sync_issue_summary(config_path=DEFAULT_CONFIG, repo_dir=None, push=True, retries=3):
    """生成/刷新并推送 issue汇总.xlsx。返回 (ok, url, detail)。"""
    rows = collect_issue_rows()
    if not rows:
        return True, "", "本地无已提 issue 记录(out_res/issues/),跳过推送"
    local_out = os.path.join(STATS_DIR_LOCAL, ISSUE_SUMMARY_NAME)
    build_issue_summary_workbook(rows, local_out)
    if not push:
        return True, local_out, "仅本地生成({} 行): {}".format(len(rows), local_out)
    try:
        cfg = json.load(open(config_path, encoding="utf-8-sig"))
    except Exception:
        cfg = {}
    repo_dir = repo_dir or cfg.get("issue_stats_repo_dir") or DEFAULT_REPO_DIR
    repo_path = "{}/{}".format(repo_dir, ISSUE_SUMMARY_NAME)
    return _push_with_merge(
        local_out, repo_path, config_path, ISSUE_HEADERS, ["aclnn接口名", "issue链接"],
        "stats: issue summary {} rows from {}".format(len(rows), socket.gethostname()),
        retries)


def main():
    parser = argparse.ArgumentParser(
        description="问题统计目录同步 GitCode 仓(Alice_zhou/issue 的 问题统计/:接口压测结果与提单汇总.xlsx + issue汇总.xlsx)")
    parser.add_argument("--local-xlsx", default=DEFAULT_LOCAL_XLSX,
                        help="本地批量压测结果表(接口汇总数据源),默认 out_res/批量压测结果.xlsx")
    parser.add_argument("--config", default=DEFAULT_CONFIG, help="配置文件路径")
    parser.add_argument("--api-summary-only", action="store_true", help="仅同步 接口压测结果与提单汇总.xlsx")
    parser.add_argument("--issue-summary-only", action="store_true", help="仅同步 issue汇总.xlsx")
    parser.add_argument("--no-push", action="store_true", help="仅本地生成两个 xlsx,不推送")
    parser.add_argument("--retries", type=int, default=3, help="并发冲突重试次数,默认 3")
    parser.add_argument("--json", action="store_true", help="以 JSON 输出结果")
    args = parser.parse_args()

    enabled, why = issue_stats_enabled(args.config)
    push = (not args.no_push) and enabled
    results = {}
    if not args.issue_summary_only:
        results["api_summary"] = sync_api_summary(args.local_xlsx, args.config,
                                                  push=push, retries=args.retries)
    if not args.api_summary_only:
        results["issue_summary"] = sync_issue_summary(args.config, push=push, retries=args.retries)

    if args.json:
        print(json.dumps({"push": push, "switch": why,
                          "api_summary": {"ok": results.get("api_summary", (None,))[0],
                                          "url": results.get("api_summary", ("", ""))[1],
                                          "detail": results.get("api_summary", ("", "", ""))[2]},
                          "issue_summary": {"ok": results.get("issue_summary", (None,))[0],
                                            "url": results.get("issue_summary", ("", ""))[1],
                                            "detail": results.get("issue_summary", ("", "", ""))[2]}},
                         ensure_ascii=False))
    else:
        if not push:
            log("STATS_SYNC", "推送关闭({}),仅本地生成".format(why))
        for name, (ok, url, detail) in results.items():
            if ok and url:
                log("STATS_SYNC", "{} 成功 → {} ({})".format(name, url, detail))
            elif ok:
                log("STATS_SYNC", "{}: {}".format(name, detail))
            else:
                log("STATS_SYNC", "{} 失败: {}".format(name, detail))
    return 0 if all(r[0] for r in results.values()) else 1


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