#!/usr/bin/env python3
"""
自动将单模型 evaluation_report.json 转换为中文 Markdown 评测报告。

用法:
    python generate_eval_report.py <evaluation_report.json 路径>
    python generate_eval_report.py <evaluation_report.json 路径> --output <输出路径>
    python generate_eval_report.py <evaluation_report.json 路径> --model "模型名称" --date 2026-03-01

示例:
    python eval_tools/model_comparison/generate_eval_report.py \
        evaluation_results/.../evaluation_report.json \
        --model yolov5s-300w-newdata-cncap
"""

import json
import argparse
import sys
from datetime import date
from pathlib import Path

# Allow importing class_config from the eval_tools root
sys.path.insert(0, str(Path(__file__).parent.parent))
from class_config import REPORT_3D_CLASS_LABELS


def fmt(v: float, decimals: int = 4) -> str:
    return f"{v:.{decimals}f}"


def fmt2(v: float) -> str:
    return f"{v:.2f}"


def pct(v: float) -> str:
    return f"{v * 100:.1f}%"


def build_report(data: dict, model_name: str, report_date: str) -> str:
    lines = []

    # ── 标题 ─────────────────────────────────────────────────────────────────
    lines.append(f"# 模型评测报告: {model_name}")
    lines.append("")
    lines.append(f"**模型**: {model_name}  ")
    lines.append(f"**评测日期**: {report_date}  ")

    eval_cfg = data.get("evaluation_config", {})
    if eval_cfg:
        lines.append(f"**置信度阈值 (P/R/F1)**: {eval_cfg.get('conf_threshold', '-')}  ")
        lines.append(f"**IoU 阈值**: {eval_cfg.get('iou_threshold', '-')}  ")
        lines.append(f"**AP 计算方法**: {eval_cfg.get('ap_method', '-')}  ")

    lines.append("")
    lines.append("---")
    lines.append("")

    # ── 2D Overall ────────────────────────────────────────────────────────────
    ov2d = data["2d_evaluation"]["overall"]
    lines.append("## 📊 2D检测指标 (Overall)")
    lines.append("")
    lines.append("| 指标 | 数值 |")
    lines.append("|------|------|")
    lines.append(f"| **Precision** | {fmt(ov2d['precision'])} |")
    lines.append(f"| **Recall** | {fmt(ov2d['recall'])} |")
    lines.append(f"| **F1-Score** | {fmt(ov2d['f1_score'])} |")
    lines.append(f"| **mAP** | {fmt(ov2d['map'])} |")
    lines.append(f"| **TP** | {ov2d['tp']:,} |")
    lines.append(f"| **FP** | {ov2d['fp']:,} |")
    lines.append(f"| **FN** | {ov2d['fn']:,} |")
    lines.append("")

    # ── 2D Per-Class ──────────────────────────────────────────────────────────
    lines.append("## 📋 2D检测指标 (Per Class)")
    lines.append("")
    lines.append("| 类别 | Precision | Recall | F1 | AP | GT | TP | FP | FN |")
    lines.append("|------|-----------|--------|----|----|-----|-----|-----|-----|")

    pc2d = data["2d_evaluation"]["per_class"]
    for cls, cd in pc2d.items():
        lines.append(
            f"| **{cls}** | {fmt(cd['precision'])} | {fmt(cd['recall'])} "
            f"| {fmt(cd['f1_score'])} | {fmt(cd['ap'])} "
            f"| {cd['num_gt']:,} | {cd['tp']:,} | {cd['fp']:,} | {cd['fn']:,} |"
        )
    lines.append("")
    lines.append("---")
    lines.append("")

    # ── 3D Overall per class ──────────────────────────────────────────────────
    m3d = data.get("3d_evaluation", {})
    if not m3d:
        return "\n".join(lines)

    lines.append("## 🎯 3D检测指标")
    lines.append("")

    CLS_LABELS = REPORT_3D_CLASS_LABELS

    LONG_RANGES = ["long_0-10m", "long_10-20m", "long_20-30m", "long_30-40m",
                   "long_40-50m", "long_50-60m", "long_60-70m", "long_70-80m",
                   "long_80-90m", "long_90-100m", "long_100-999m"]

    for cls_key, cls_label in CLS_LABELS.items():
        if cls_key not in m3d:
            continue
        cd = m3d[cls_key]
        ov = cd["overall"]
        n = ov["num_samples"]

        lines.append(f"### {cls_label} ({n:,} 样本)")
        lines.append("")

        # Overall stats
        lines.append("#### 总体指标")
        lines.append("")
        lines.append("| 指标 | Mean | Median | Std | P90 |")
        lines.append("|------|------|--------|-----|-----|")

        def stat_row(label, key):
            s = ov[key]
            return (f"| **{label}** | {fmt2(s['mean'])} | {fmt2(s['median'])} "
                    f"| {fmt2(s['std'])} | {fmt2(s['percentile_90'])} |")

        lines.append(stat_row("Lateral Error (m)",         "lateral_error"))
        lines.append(stat_row("Longitudinal Error (m)",    "longitudinal_error"))
        lines.append(stat_row("Long. Relative Error",      "longitudinal_relative_error"))
        lines.append(stat_row("Heading Error (rad)",       "heading_error"))
        lines.append(stat_row("Heading Error Relaxed",     "heading_error_relaxed"))
        rev_pct = ov['reversal_percentage']
        lines.append(f"| **Reversal** | {ov['reversal_count']:,} ({rev_pct:.2f}%) | - | - | - |")
        lines.append("")

        # Distance range breakdown
        avail_ranges = [r for r in LONG_RANGES if r in cd]
        if avail_ranges:
            lines.append("#### 按距离分段 (纵向误差 Mean / Lateral Mean / Samples)")
            lines.append("")
            lines.append("| 距离段 | 样本数 | Lateral (m) | Longitudinal (m) | Long.Rel | Heading | Reversal% |")
            lines.append("|--------|--------|-------------|------------------|----------|---------|-----------|")
            for rng in avail_ranges:
                r = cd[rng]
                rov = r
                rn = rov["num_samples"]
                if rn == 0:
                    continue
                lat  = rov["lateral_error"]["mean"]
                lon  = rov["longitudinal_error"]["mean"]
                lrel = rov["longitudinal_relative_error"]["mean"]
                hd   = rov["heading_error"]["mean"]
                rev  = rov["reversal_percentage"]
                lines.append(
                    f"| {rng.replace('long_', '')} | {rn:,} | {fmt2(lat)} | {fmt2(lon)} "
                    f"| {lrel:.3f} | {fmt2(hd)} | {rev:.1f}% |"
                )
            lines.append("")

    lines.append("---")
    lines.append("")

    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(
        description="将单模型 evaluation_report.json 转换为中文 Markdown 评测报告"
    )
    parser.add_argument("json_path", help="evaluation_report.json 的路径")
    parser.add_argument("--output", "-o", default=None,
                        help="输出 Markdown 文件路径（默认与 JSON 同目录，文件名 EVALUATION_REPORT.md）")
    parser.add_argument("--model", default=None,
                        help="模型名称（默认从目录名推断）")
    parser.add_argument("--date", default=str(date.today()),
                        help="评测日期 (默认今天，格式 YYYY-MM-DD)")
    args = parser.parse_args()

    json_path = Path(args.json_path).resolve()
    if not json_path.exists():
        print(f"错误: 文件不存在: {json_path}", file=sys.stderr)
        sys.exit(1)

    with open(json_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    # 从路径推断模型名称：取 json 所在目录的上级目录名
    if args.model:
        model_name = args.model
    else:
        # e.g. .../yolov5s-300w-newdata-cncap/20260228_102849/evaluation_report.json
        model_name = json_path.parent.parent.name
        if not model_name or model_name == ".":
            model_name = json_path.parent.name
    print(f"模型: {model_name}")

    report = build_report(data, model_name, args.date)

    if args.output:
        out_path = Path(args.output)
    else:
        out_path = json_path.parent / "EVALUATION_REPORT.md"

    out_path.write_text(report, encoding="utf-8")
    print(f"报告已生成: {out_path}")


if __name__ == "__main__":
    main()
