# -*- coding: utf-8 -*-
"""
生成培训试卷（Word，15题，答案附末尾）。

固定格式：单选6×5分 + 多选4×10分 + 判断5×6分 = 100分

关键功能：答案布局随机校验 —— 正确选项位置必须打散，不允许扎堆某个字母。

用法:
    python build_exam.py --config exam.json --out "输出文件.docx"
    python build_exam.py --config exam.json --check-only    # 只校验答案分布
"""
import argparse
import json
import sys
from collections import Counter
from pathlib import Path

import docx
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Pt, RGBColor

FONT = "宋体"
TITLE_FONT = "黑体"

SPEC = {
    "单选": {"count": 6, "score": 5, "label": "一、单项选择题", "note": "每题5分，共30分"},
    "多选": {"count": 4, "score": 10, "label": "二、多项选择题", "note": "每题10分，共40分，多选、漏选、错选均不得分"},
    "判断": {"count": 5, "score": 6, "label": "三、判断题", "note": "每题6分，共30分，正确的打“√”，错误的打“×”"},
}


# ---------------- 答案分布校验 ----------------

def check_answer_spread(cfg):
    """校验正确答案的位置是否打散。返回 (是否通过, 问题列表)。"""
    problems = []

    # 单选：A/B/C/D 每个字母不允许出现 3 次以上
    single = [q["答案"].strip().upper() for q in cfg["单选"]]
    c = Counter(single)
    for letter, n in c.items():
        if n > 2:
            problems.append(f"单选题答案 {letter} 出现 {n} 次（上限 2 次）：{single}")
    missing = [x for x in "ABCD" if x not in c]
    if len(missing) > 1:
        problems.append(f"单选题答案只用到 {sorted(c)}，{missing} 完全没出现，分布太集中")

    # 判断：正确/错误数量差不超过 1
    judge = [str(q["答案"]).strip() for q in cfg["判断"]]
    n_true = sum(1 for a in judge if a in ("√", "正确", "对", "T", "True"))
    n_false = len(judge) - n_true
    if abs(n_true - n_false) > 1:
        problems.append(f"判断题 正确{n_true}题 / 错误{n_false}题，差值超过 1")

    # 多选：正确项组合不能重复
    multi = ["".join(sorted(q["答案"].strip().upper())) for q in cfg["多选"]]
    dup = [k for k, v in Counter(multi).items() if v > 1]
    if dup:
        problems.append(f"多选题答案组合重复：{dup}（{multi}）")

    return (len(problems) == 0), problems


def check_counts(cfg):
    problems = []
    total = 0
    for key, spec in SPEC.items():
        n = len(cfg.get(key, []))
        if n != spec["count"]:
            problems.append(f"{key}题应为 {spec['count']} 题，实际 {n} 题")
        total += n * spec["score"]
    if total != 100:
        problems.append(f"总分 {total}，应为 100")
    return (len(problems) == 0), problems


# ---------------- 文档生成 ----------------

def para(doc, text, size=10.5, bold=False, align=WD_ALIGN_PARAGRAPH.LEFT,
         font=FONT, space_after=2, color=None):
    p = doc.add_paragraph()
    p.alignment = align
    pf = p.paragraph_format
    pf.space_after = Pt(space_after)
    pf.space_before = Pt(0)
    pf.line_spacing = 1.0
    r = p.add_run(text)
    r.font.size = Pt(size)
    r.font.bold = bold
    r.font.name = font
    r._element.rPr.rFonts.set(docx.oxml.ns.qn("w:eastAsia"), font)
    if color:
        r.font.color.rgb = color
    return p


def build(cfg, out_path):
    doc = docx.Document()
    sec = doc.sections[0]
    # 一页纸：压缩页边距
    sec.top_margin = Pt(38)
    sec.bottom_margin = Pt(34)
    sec.left_margin = Pt(48)
    sec.right_margin = Pt(48)

    theme = cfg["培训主题"]
    para(doc, f"{theme}培训考试试卷", 16, True, WD_ALIGN_PARAGRAPH.CENTER, TITLE_FONT, 4)
    para(doc, "姓名：____________    班组/单位：____________    得分：__________",
         10.5, False, WD_ALIGN_PARAGRAPH.CENTER, FONT, 8)

    qno = 0
    for key in ("单选", "多选", "判断"):
        spec = SPEC[key]
        para(doc, f"{spec['label']}（{spec['note']}）", 11, True, font=TITLE_FONT, space_after=3)
        for q in cfg[key]:
            qno += 1
            if key == "判断":
                para(doc, f"{q['题号']}.（    ）{q['题干']}", 10.5, space_after=2)
            else:
                para(doc, f"{q['题号']}.{q['题干']}（    ）", 10.5, space_after=1)
                opts = q["选项"]
                # 选项两栏排布，节省纸张
                if key == "单选" and all(len(v) <= 14 for v in opts.values()):
                    keys = sorted(opts)
                    line1 = "    ".join(f"{k}. {opts[k]}" for k in keys[:2])
                    line2 = "    ".join(f"{k}. {opts[k]}" for k in keys[2:])
                    para(doc, "    " + line1, 10.5, space_after=0)
                    para(doc, "    " + line2, 10.5, space_after=2)
                else:
                    for k in sorted(opts):
                        para(doc, f"    {k}. {opts[k]}", 10.5, space_after=0)
                    doc.paragraphs[-1].paragraph_format.space_after = Pt(2)

    # ---- 参考答案 ----
    doc.add_page_break()
    para(doc, "———— 以下为参考答案，发给学员前请删除本页 ————",
         10.5, True, WD_ALIGN_PARAGRAPH.CENTER, TITLE_FONT, 8,
         color=RGBColor(0xC0, 0x00, 0x00))
    para(doc, "参考答案", 14, True, WD_ALIGN_PARAGRAPH.CENTER, TITLE_FONT, 6)

    for key in ("单选", "多选", "判断"):
        spec = SPEC[key]
        answers = "    ".join(f"{q['题号']}. {q['答案']}" for q in cfg[key])
        para(doc, f"{spec['label']}（{spec['note'].split('，')[0]}）", 11, True,
             font=TITLE_FONT, space_after=2)
        para(doc, "    " + answers, 10.5, space_after=6)

    Path(out_path).parent.mkdir(parents=True, exist_ok=True)
    doc.save(out_path)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", required=True)
    ap.add_argument("--out")
    ap.add_argument("--check-only", action="store_true")
    args = ap.parse_args()

    cfg = json.loads(Path(args.config).read_text(encoding="utf-8"))

    ok_c, prob_c = check_counts(cfg)
    ok_a, prob_a = check_answer_spread(cfg)

    for p in prob_c + prob_a:
        print(f"  ⚠ {p}")

    if not (ok_c and ok_a):
        print("\n校验不通过。调整题目/选项顺序后重新生成——不要直接出卷。")
        return 1

    single = [q["答案"] for q in cfg["单选"]]
    judge = [q["答案"] for q in cfg["判断"]]
    print(f"  答案分布校验通过  单选:{' '.join(single)}   判断:{' '.join(judge)}")

    if args.check_only:
        return 0
    if not args.out:
        print("  [错误] 需要 --out")
        return 1

    build(cfg, args.out)
    print(f"  {Path(args.out).name}   15题 / 100分 / 答案附末尾")
    return 0


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