#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""定时更新机制(每日 4 次,默认整点 00:00/06:00/12:00/18:00)

每轮自动执行三件事:
  1. 框架自动更新:git pull --rebase --autostash 拉取 Agent 框架仓
     (https://gitcode.com/Alice_zhou/Vector_fuzz_cpp_testing,含 AGENTS.md/scripts/skills/
     turing_sec)——多台压测机各自定时拉取,保持框架版本一致;本地未提交改动由
     autostash 自动保护;**压测进程(aclnnAPI*)运行中自动跳过本轮框架更新**(防止
     中断在跑任务/编译产物被触碰),等待下一轮;turing_sec 若为独立克隆仓(含自己的
     .git)同样 pull
  2. 结果表格兜底同步:补推 out_res/批量压测结果.xlsx 至 GitCode 仓 Alice_zhou/issue
     (多机安全合并,同 sync_batch_results_git.py;实时同步失败/漏推的兜底,
       文件不存在则静默跳过)
  3. 每日 issue 在线表格兜底同步:补推当日已提 issue 汇总表(out_res/issues/{今日}/,
     表格含 aclnn接口名/测试场景/问题/issue链接)至 GitCode 仓 issue日报_{今日}.xlsx
     (同 sync_daily_issues_git.py;提单后实时同步失败/漏推的兜底,当日无提单记录则静默跳过)
  4. 问题统计目录兜底同步:补推 问题统计/接口压测结果与提单汇总.xlsx(每次压测结果依次填充,
     三态着色 pass绿/待复核黄/fail红)与 问题统计/issue汇总.xlsx(同 sync_issue_stats_git.py;
     实时同步失败/漏推的兜底,无数据则静默跳过)

安装(一次性,执行机上):
  python scripts/scheduled_update.py --install-cron                 # Linux: crontab;Windows: 打印 schtasks(管理员执行)
  python scripts/scheduled_update.py --install-cron --hours 0,6,12,18   # 自定义整点
  python scripts/scheduled_update.py --uninstall-cron                # 移除定时任务

手动执行(定时任务入口,也可立即跑一轮):
  python scripts/scheduled_update.py

日志: out_res/scheduled_update.log(追加式;stdout 同步输出,cron 重定向亦写入该文件)
"""
import argparse
import contextlib
import os
import socket
import subprocess
import sys
import time

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

SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
LOG_PATH = os.path.join(PROJECT_ROOT, "out_res", "scheduled_update.log")
DEFAULT_HOURS = "0,6,12,18"
CRON_MARKER = "scheduled_update.py"
WIN_TASK_PREFIX = "FuzzScheduledUpdate"


def log(msg):
    line = "[{}] [{}] {}".format(time.strftime("%Y-%m-%d %H:%M:%S"), socket.gethostname(), msg)
    print(line)
    with contextlib.suppress(Exception):
        os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
        with open(LOG_PATH, "a", encoding="utf-8") as fh:
            fh.write(line + "\n")


def fuzz_processes_running():
    """检测是否有压测进程在跑(aclnnAPI* 可执行/样例执行器),检测失败不阻塞更新。"""
    try:
        if os.name == "posix":
            r = subprocess.run(["pgrep", "-af", "aclnnAPI"],
                               capture_output=True, text=True, timeout=30)
            return r.returncode == 0 and r.stdout.strip() != ""
        r = subprocess.run(["tasklist", "/FO", "CSV"],
                           capture_output=True, text=True, timeout=30)
        return any("aclnnAPI" in line for line in r.stdout.splitlines())
    except Exception:
        return False


def git_pull(repo_path):
    """git pull --rebase --autostash(本地未提交改动自动 stash 保护),返回 (ok, 摘要)。"""
    try:
        r = subprocess.run(["git", "-C", repo_path, "pull", "--rebase", "--autostash"],
                           capture_output=True, text=True, timeout=600)
        out = ((r.stdout or "") + (r.stderr or "")).strip()
        lines = [ln for ln in out.splitlines() if ln.strip()]
        summary = " | ".join(lines[-3:]) if lines else "no output"
        return r.returncode == 0, summary
    except Exception as exc:
        return False, str(exc)


def update_framework(root):
    """步骤1:框架自动更新(agent 仓 + 独立克隆的 turing_sec 仓)。"""
    if fuzz_processes_running():
        log("SKIP 框架更新:检测到压测进程(aclnnAPI*)运行中,本轮跳过,等待下一轮")
        return
    ok, summary = git_pull(root)
    log("框架仓 git pull {}: {}".format("OK" if ok else "FAIL", summary))
    if not ok:
        return
    tsec = os.path.join(root, "turing_sec")
    if os.path.isdir(os.path.join(tsec, ".git")):  # turing_sec 独立克隆部署(非本仓内嵌目录)
        ok2, summary2 = git_pull(tsec)
        log("turing_sec git pull {}: {}".format("OK" if ok2 else "FAIL", summary2))


def push_results_fallback(root):
    """步骤2:批量压测结果.xlsx 定时兜底推送 GitCode 仓(Alice_zhou/issue)。"""
    xlsx = os.path.join(root, "out_res", "批量压测结果.xlsx")
    if not os.path.isfile(xlsx):
        log("SKIP 结果兜底同步:out_res/批量压测结果.xlsx 不存在")
        return
    try:
        sys.path.insert(0, SCRIPTS_DIR)
        from sync_batch_results_git import sync_local_xlsx
        ok, url, detail = sync_local_xlsx(
            xlsx, commit_msg="results: scheduled fallback sync from {}".format(socket.gethostname()))
        if ok:
            log("结果表格兜底同步 OK: {} ({})".format(url or "Alice_zhou/issue", detail))
        else:
            log("结果表格兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail))
    except Exception as exc:
        log("结果表格兜底同步异常(不阻塞,下轮重试): {}".format(exc))


def push_daily_issues_fallback(root):
    """步骤3:当日 issue 在线表格兜底推送(out_res/issues/{今日}/ → GitCode 仓 issue日报_{今日}.xlsx)。

    当日无提单记录(daily_issues.json/filed.json 均无)时静默跳过;失败不阻塞(下轮重试)。"""
    date_str = time.strftime("%Y-%m-%d")
    day_dir = os.path.join(root, "out_res", "issues", date_str)
    if not os.path.isdir(day_dir):
        return
    if not any(os.path.isfile(os.path.join(day_dir, n)) for n in ("daily_issues.json", "filed.json")):
        return
    try:
        sys.path.insert(0, SCRIPTS_DIR)
        from sync_daily_issues_git import sync_daily_issues
        ok, url, detail = sync_daily_issues(date_str)
        if ok and url:
            log("每日issue表格兜底同步 OK: {} ({})".format(url, detail))
        elif ok:
            log("SKIP 每日issue表格兜底同步: {}".format(detail))
        else:
            log("每日issue表格兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail))
    except Exception as exc:
        log("每日issue表格兜底同步异常(不阻塞,下轮重试): {}".format(exc))


def push_issue_stats_fallback(root):
    """步骤4:问题统计目录兜底推送(out_res/问题统计/ 两表 → GitCode 仓 问题统计/)。

    接口压测结果与提单汇总.xlsx(数据源本地 out_res/批量压测结果.xlsx)+ issue汇总.xlsx
    (数据源 out_res/issues/);无数据/文件缺失静默跳过;失败不阻塞(下轮重试)。"""
    xlsx = os.path.join(root, "out_res", "批量压测结果.xlsx")
    if not os.path.isfile(xlsx):
        log("SKIP 问题统计兜底同步:out_res/批量压测结果.xlsx 不存在")
        return
    try:
        sys.path.insert(0, SCRIPTS_DIR)
        from sync_issue_stats_git import issue_stats_enabled, sync_api_summary, sync_issue_summary
        enabled, why = issue_stats_enabled()
        if not enabled:
            log("SKIP 问题统计兜底同步: 开关关闭({})".format(why))
            return
        ok1, url1, detail1 = sync_api_summary(xlsx)
        if ok1 and url1:
            log("问题统计-接口压测结果与提单汇总 兜底同步 OK: {} ({})".format(url1, detail1))
        elif not ok1:
            log("问题统计-接口压测结果与提单汇总 兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail1))
        ok2, url2, detail2 = sync_issue_summary()
        if ok2 and url2:
            log("问题统计-issue汇总 兜底同步 OK: {} ({})".format(url2, detail2))
        elif not ok2:
            log("问题统计-issue汇总 兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail2))
    except Exception as exc:
        log("问题统计兜底同步异常(不阻塞,下轮重试): {}".format(exc))


def run_once(root):
    log("========== 定时更新开始(每日 {} 整点轮) ==========".format(DEFAULT_HOURS))
    update_framework(root)
    push_results_fallback(root)
    push_daily_issues_fallback(root)
    push_issue_stats_fallback(root)
    log("========== 定时更新结束 ==========")
    return 0


def parse_hours(hours_str):
    hours = []
    for part in str(hours_str).split(","):
        part = part.strip()
        if not part:
            continue
        try:
            h = int(part)
        except ValueError:
            raise SystemExit("[ERROR] 非法整点值: {}(格式如 0,6,12,18)".format(part))
        if not 0 <= h <= 23:
            raise SystemExit("[ERROR] 整点值越界: {}(0~23)".format(h))
        hours.append(h)
    if not hours:
        raise SystemExit("[ERROR] 至少指定一个整点(如 --hours 0,6,12,18)")
    return sorted(set(hours))


def install_cron(root, hours):
    if os.name == "posix":
        cron_line = "0 {} * * * cd {} && python3 scripts/scheduled_update.py >> out_res/scheduled_update.log 2>&1".format(
            ",".join(str(h) for h in hours), root)
        current = os.popen("crontab -l 2>/dev/null").read()
        if CRON_MARKER in current:
            print("[CRON] crontab 已包含 scheduled_update 定时任务,跳过安装")
            return 0
        new_crontab = (current.rstrip("\n") + "\n" if current.strip() else "") + cron_line + "\n"
        os.system("echo '{}' | crontab -".format(new_crontab.replace("'", "'\\''")))
        print("[CRON] 已安装每日 {} 整点定时更新任务(共 {} 轮): {}".format(
            "/".join("{:02d}:00".format(h) for h in hours), len(hours), cron_line))
        return 0
    # Windows:打印 schtasks 命令(需管理员执行;schtasks DAILY 单任务仅支持一个时刻,逐整点建任务)
    print("[CRON] Windows 环境请以管理员身份执行以下命令安装计划任务(每日 {} 轮):".format(len(hours)))
    for h in hours:
        print('  schtasks /Create /TN "{}_{:02d}" /SC DAILY /ST {:02d}:00 /TR '
              '"cmd /c cd /d {} && python scripts\\scheduled_update.py >> out_res\\scheduled_update.log 2>&1" /F'
              .format(WIN_TASK_PREFIX, h, h, root))
    return 0


def uninstall_cron(hours):
    if os.name == "posix":
        current = os.popen("crontab -l 2>/dev/null").read()
        kept = [ln for ln in current.splitlines() if CRON_MARKER not in ln]
        os.system("printf '%s\\n' '{}' | crontab -".format("\\n".join(kept).replace("'", "'\\''")))
        print("[CRON] 已移除 scheduled_update 定时任务")
        return 0
    print("[CRON] Windows 环境请执行:")
    for h in hours:
        print('  schtasks /Delete /TN "{}_{:02d}" /F'.format(WIN_TASK_PREFIX, h))
    return 0


def main():
    parser = argparse.ArgumentParser(
        description="定时更新机制(每日 4 次整点:框架仓 git pull 自动更新 + 批量压测结果.xlsx 兜底推送 GitCode 仓)")
    parser.add_argument("--hours", default=DEFAULT_HOURS,
                        help="每日执行的整点列表,默认 0,6,12,18(每日 4 次)")
    parser.add_argument("--install-cron", action="store_true",
                        help="安装定时任务(Linux: crontab / Windows: 打印 schtasks 命令)后退出")
    parser.add_argument("--uninstall-cron", action="store_true", help="移除定时任务后退出")
    args = parser.parse_args()

    hours = parse_hours(args.hours)
    if args.install_cron:
        return install_cron(PROJECT_ROOT, hours)
    if args.uninstall_cron:
        return uninstall_cron(hours)
    return run_once(PROJECT_ROOT)


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