#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SGO 崩溃解码工具 (Tier 1 本地分析) — 配合 LINGS「故障诊断」页。

把崩溃报告里的 backtrace 裸地址 + 对应版本归档的 firmware.elf 解成 函数:文件:行号。
也支持 Phase 2 的完整 coredump blob (调 IDF 的 espcoredump.py 看完整栈/寄存器/各任务)。

从故障诊断详情页下载:① 故障包 zip (最省事, report.json + firmware.elf 已打好)
  或单独 ① 该版本 firmware.elf  ② (可选) coredump blob
然后本机跑:

  # 0) 一键 (推荐): 详情页"下载故障包" → 解包内含的 elf 自动对 report 里的 backtrace 解
  python sgo_crash_decode.py --bundle crash_L51126210001_seq2.zip

  # 1) 直接给地址 (详情页复制的 backtrace, hex 或 dec, 空格/逗号分隔)
  python sgo_crash_decode.py --elf firmware.elf --bt "0x40375f76 0x42044e53 0x42048da7"

  # 2) 给崩溃记录 JSON (详情页"复制 JSON" / 设备 /json/diag 的单条对象)
  python sgo_crash_decode.py --elf firmware.elf --record crash.json

  # 3) 完整 coredump (Phase 2: 网页下载的 coredump blob)
  python sgo_crash_decode.py --elf firmware.elf --coredump dump.bin

前提: 已激活 ESP-IDF 环境 (. $IDF_PATH/export.ps1  或  export.sh),
      PATH 里有 xtensa-esp32s3-elf-addr2line; --coredump 模式还需 IDF_PATH。
      (你的开发机本来就有这套工具链。)
"""
import argparse
import json
import os
import shutil
import subprocess
import sys

ADDR2LINE = "xtensa-esp32s3-elf-addr2line"   # ESP32-S3 = Xtensa


def die(msg):
    print("✗ " + msg, file=sys.stderr)
    sys.exit(1)


def find_addr2line(override):
    if override:
        return override
    p = shutil.which(ADDR2LINE)
    if p:
        return p
    die(f"找不到 {ADDR2LINE}。请先激活 ESP-IDF 环境 "
        f"(. $IDF_PATH/export.ps1 或 source $IDF_PATH/export.sh), 或用 --addr2line 指定路径。")


def parse_addr(v):
    if isinstance(v, int):
        return v
    s = str(v).strip()
    if s == "":
        return None
    return int(s, 16) if s.lower().startswith("0x") else int(s)


def load_addrs(args):
    addrs = []
    if args.bt:
        for tok in args.bt.replace(",", " ").split():
            a = parse_addr(tok)
            if a is not None:
                addrs.append(a)
    if args.record:
        with open(args.record, encoding="utf-8") as f:
            rec = json.load(f)
        if isinstance(rec, list):                 # 整个 /json/diag 数组 → 取第一条
            rec = rec[0] if rec else {}
        # 兼容: admin 报告 (backtrace_pcs) / 设备 /json/diag (bt)
        bt = rec.get("backtrace_pcs") or rec.get("bt") or []
        for v in bt:
            a = parse_addr(v)
            if a is not None:
                addrs.append(a)
        # 顺手把崩溃 PC 放最前 (若 bt 里没有)
        for k in ("crash_pc", "pc"):
            pc = rec.get(k)
            if pc is not None:
                a = parse_addr(pc)
                if a is not None and a not in addrs:
                    addrs.insert(0, a)
                break
    return addrs


def decode_addrs(a2l, elf, addrs):
    if not addrs:
        die("没有 backtrace 地址。用 --bt 或 --record 提供。")
    hexaddrs = [f"0x{a:08x}" for a in addrs]
    print(f"→ {a2l}\n→ elf: {elf}\n→ {len(hexaddrs)} 帧\n" + "-" * 60)
    res = subprocess.run([a2l, "-pfiaC", "-e", elf, *hexaddrs],
                         capture_output=True, text=True)
    if res.returncode != 0:
        die("addr2line 失败:\n" + (res.stderr or res.stdout))
    sys.stdout.write(res.stdout)


def decode_coredump(elf, blob):
    idf = os.environ.get("IDF_PATH")
    if not idf:
        die("--coredump 需 IDF_PATH 环境变量 (激活 ESP-IDF)。")
    espcoredump = os.path.join(idf, "components", "espcoredump", "espcoredump.py")
    if not os.path.exists(espcoredump):
        die(f"找不到 {espcoredump}")
    print(f"→ espcoredump.py info_corefile (chip esp32s3)\n" + "-" * 60)
    subprocess.run([sys.executable, espcoredump, "info_corefile",
                    "-t", "elf", "--chip", "esp32s3", "-c", blob, elf])


def main():
    ap = argparse.ArgumentParser(
        description="SGO 崩溃解码 (addr2line 解 backtrace / espcoredump 解完整 coredump)")
    ap.add_argument("--bundle",
                    help="故障包 zip (LINGS 详情页'下载故障包': report.json + firmware.elf), 一键解码")
    ap.add_argument("--elf",
                    help="对应版本归档的 firmware.elf (从故障诊断页下载; --bundle 时自动从包内取)")
    ap.add_argument("--bt", help="backtrace 地址, 空格/逗号分隔 (hex 或 dec)")
    ap.add_argument("--record", help="崩溃记录 JSON 文件 (含 backtrace_pcs / bt)")
    ap.add_argument("--coredump", help="完整 coredump blob 文件 (Phase 2)")
    ap.add_argument("--addr2line", help="覆盖 addr2line 路径 (默认从 PATH 找)")
    args = ap.parse_args()

    # 故障包: 解包取 elf + report.json, 直接对包内 backtrace 解 (最省事)
    if args.bundle:
        if not os.path.exists(args.bundle):
            die(f"找不到故障包: {args.bundle}")
        import tempfile
        import zipfile
        tmpd = tempfile.mkdtemp(prefix="sgo_bundle_")
        with zipfile.ZipFile(args.bundle) as z:
            z.extractall(tmpd)
        elf = os.path.join(tmpd, "firmware.elf")
        rec = os.path.join(tmpd, "report.json")
        if not os.path.isfile(elf):
            die("故障包内无 firmware.elf (该版本上传时没带 elf, 无法解函数:行号)。\n"
                "  → 重新用上传表单带 firmware.elf 发该版本后再下载故障包。")
        if not os.path.isfile(rec):
            die("故障包内无 report.json (包损坏?)。")
        print(f"→ 故障包: {args.bundle}  (已解包到 {tmpd})")
        args.elf = elf
        if not args.record and not args.bt:
            args.record = rec

    if not args.elf:
        die("需 --elf 或 --bundle 提供 firmware.elf。")
    if not os.path.exists(args.elf):
        die(f"找不到 elf: {args.elf}")

    if args.coredump:
        if not os.path.exists(args.coredump):
            die(f"找不到 coredump: {args.coredump}")
        decode_coredump(args.elf, args.coredump)
    else:
        a2l = find_addr2line(args.addr2line)
        decode_addrs(a2l, args.elf, load_addrs(args))


if __name__ == "__main__":
    main()
