# -*- coding: utf-8 -*-
r"""把《AMC8 真题》（2000–2025，25 套 / 625 题，零答案）拍平成「组卷台」归一化 schema，
作为一个新教材并入同一个组卷台。

由 gen_组卷台.py 调用（与 zujuan_exam / zujuan_v2 / zujuan_cn 同款两处 hook）：
  data_recs, course_sub, fig_dict = zujuan_amc8.load(HERE)
  data += data_recs
  course['AMC8真题'] = course_sub
  FIG.update(fig_dict)

## 数据来源
`~/Desktop/AI workspace/AMC8考情分析/题库/`（另一个 surface 录的，见那边 README）：
  amc8_questions.jsonl  id/year/no/stem_en/stem_zh/options/options_are_figures/figure/figures
  amc8_answers.jsonl    **答案在这里，绝不读它**（零答案红线）
  figures/*.png         AoPS 官方渲染的几何图，228 张

## 轴怎么摆（2026-09-05 与组卷台负责窗口 surface:3 对过口径，按它定的来）
  tb 教材 = AMC8真题
  gg 年级 = **逐年**（2025年 … 2000年，25 个，倒序）
            —— 2026-09-05 晚陆老师拍板：老师找 AMC8 就是按年找，逐年直观。
               （曾按年代段分 3 组，理由是一年只有 1 节选完年级没得选；已否决。）
  cs/cd   = **一年一节**：cs=年份倒序序号，cd=`2023年 AMC8`（与中考批「一卷一节」同款，
            选完年份默认「全部节/专题」就是整卷）。
            ⚠ 题号分档**不进 cd** —— cd 只放原卷声明的结构，分档是我们推的，不能混进去。
  s/src   = 同 cd（年份卷名）
  t 题型  = 选择题（AMC8 全卷 25 题都是五选一）
  d 难度  = **按题号映射 2/3/4**（1-8→2 易、9-16→3 中、17-25→4 难），对上 diffOf 的胡小群刻度。
            ⚠ 这是按 **MAA 题序难度递增惯例**推断的，**不是原卷声明的难度**。
              AMC8 官方从不发布逐题难度；这个映射只是让老师能筛「压轴题」，别当权威数据引用。

## 题面
`q` **只放一种语言**：有中译用中译，没中译的 81 题用英文（原料就没有，见那边 README）。
英文原题另存 `qe` 字段（544 题带；本身就是英文题的留空），**本版前端不显示不导出**，
留给以后做双语时用。选项并进 `q`，题干后换行、用制表符分隔；选项是图的写
`A. ![](asset://…)`。公式统一 `\(…\)`（组卷台契约 3.1）。

## 零答案
只读 `amc8_questions.jsonl`。`amc8_answers.jsonl` **一个字都不碰**。
load() 结束自查一次：题面里不许出现「答案是 X」「故选 X」这类痕迹。
"""
import json
import os
import re
from collections import OrderedDict

from PIL import Image                      # noqa: F401  (与 _fig_b64 同一套依赖)

from zujuan_exam import _fig_b64           # 图片编码复用上海批那套，别写第二份

TB = 'AMC8真题'
# 题号 → 难度（胡小群刻度：2 易 / 3 中 / 4 难）。⚠ 推断值，非原卷声明，见 docstring。
DIFF_BANDS = [(1, 8, 2), (9, 16, 3), (17, 25, 4)]
ANS_LEAK = re.compile(r'【\s*(答案|解析|详解)\s*】|故选\s*[:：]?\s*[A-E]|'
                      r'(?:答案是|正确答案是|The answer is|Answer\s*[:：])\s*\(?[A-E]\)?(?![a-zA-Z])')


def _diff(no):
    for lo, hi, d in DIFF_BANDS:
        if lo <= no <= hi:
            return d
    return 0


def _find_bank(HERE):
    """→ AMC8 题库目录。找不到返回 None（构建照常进行，只是少这套教材）。"""
    p = os.path.normpath(os.path.join(HERE, '..', '..', '..', 'AMC8考情分析', '题库'))
    return p if os.path.exists(os.path.join(p, 'amc8_questions.jsonl')) else None


# ⚠ `\$` 是**转义的美元符号**（题目里的钱，如 `$\$1.43$`），不是公式定界符。
#   旧正则 `\$([^$\n]+?)\$` 会把 `$\$1.43$` 的头两个 $ 配成一对，渲成 `\(\)1.43$`，
#   公式当场烂掉（2009_11 / 2015_20 / 2019_01 / 2020_02 / 2023_14 / 2024_08 六题中招，
#   2023_14 连中文题面都坏）。开定界符要求前面不是反斜杠，内容允许 `\x` 这种转义对。
TEXPAIR = re.compile(r'(?<!\\)\$((?:\\.|[^$\n])+?)\$')


def _tex(t):
    r"""`$…$` → `\\(…\\)`（组卷台契约 3.1：段内公式一律 \\(…\\)）。只换成对的、不碰 `\$`。"""
    return TEXPAIR.sub(lambda m: '\\(' + m.group(1) + '\\)', t or '')


# 选项值在题库里是**裸 LaTeX 片段**：parse_aops.py 存的时候主动把 `$` 剃掉了
# （全库 3125 个选项格，含 `$` 的 0 个）。而组卷台的 MathJax 只认 \(…\)/\[…\]，
# 不补定界符就会原样把源码打在页面上 —— `A. \frac{1}{1024}`、`A. \text{Art}`。
# 实测 194 题 / 879 格中招，占 AMC8 的 31%，是老师一眼就能看见的那种坏。
_HASTEX = re.compile(r'\\[A-Za-z]+|[\^_]')
_DONE = re.compile(r'\\\(.*?\\\)', re.S)


def _optval(v):
    r"""选项值 → 可渲染文本。纯数字/纯中文原样；图选项原样；含 TeX 的补上 \(…\)。"""
    v = (v or '').strip()
    if not v or v.startswith('!['):        # 空值、或图选项 `![](asset://…)`
        return v
    v = _tex(v)                            # 万一哪天存进来的是 `$…$`
    # 剥掉尾部游离反斜杠（AoPS 的换行 `\\` / 细空格 `\ ` 残渣，6 格中招）。
    # ⚠ 留着它会把结尾的 `\)` 转义成 `\\)`，整格渲不出来 —— 而且不报错。
    while v.rstrip().endswith('\\'):
        v = v.rstrip()[:-1]
    v = v.strip()
    if v and _HASTEX.search(_DONE.sub('', v)):   # 已经包好的部分不重复计入
        v = '\\(' + v + '\\)'
    return v


def _stem(q):
    """→ (q, qe)。q 只放一种语言（有中译用中译），qe 存英文原题备用。"""
    zh = _tex((q.get('stem_zh') or '').strip())
    en = _tex((q.get('stem_en') or '').strip())
    body = zh or en
    opts = q.get('options') or {}
    if opts:
        body += '\n' + '\t'.join('%s. %s' % (k, _optval(opts[k]))
                                  for k in 'ABCDE' if k in opts)
    return body, (en if zh else '')


def load(HERE, verbose=True):
    """返回 (data_records, course_sublist_by_year, fig_base64_dict)。"""
    B = _find_bank(HERE)
    if not B:
        if verbose:
            print('  [AMC8] 未找到 AMC8考情分析/题库/amc8_questions.jsonl，跳过')
        return [], OrderedDict(), {}

    qs = [json.loads(l) for l in
          open(os.path.join(B, 'amc8_questions.jsonl'), encoding='utf-8')]
    data, fig_refs, leak, n_zh = [], {}, 0, 0
    for q in qs:
        no = int(q['no'])
        stem, qe = _stem(q)
        if ANS_LEAK.search(stem):
            leak += 1
        if (q.get('stem_zh') or '').strip():
            n_zh += 1
        ims = []
        for rel in (q.get('figures') or []):
            # ⚠ FIG 键必须用**图片文件名词干**，不能用下标。
            #   parse_aops.py 把图选项写成 `![](asset://AMC8_2006_10_f3)`（按文件名），
            #   这边却按下标注册成 `am/AMC8_2006_10_2`，两边对不上 → figByAsset 查不到 →
            #   **五个选项全渲成空白**，页面不报错、静态检查照样打勾（2006_10 就是这么坏的）。
            key = 'am/' + os.path.splitext(os.path.basename(rel))[0]
            fp = os.path.join(B, rel)
            if os.path.exists(fp):
                fig_refs[key] = fp
                ims.append(key)
        # ⟦FIG⟧ 是 parse_aops 留下的**原位**占位符（图在原题里的位置）。
        # ⚠ 以前只把图追加到题面末尾，占位符原样留在正文 —— 老师看到的是「⟦FIG⟧」四个怪字
        #   （中译里 6 题中招）。改成按序就地替换，多出来的图才追加到末尾。
        refs = ['![](asset://%s)' % k[3:] for k in ims]
        used = 0
        while '⟦FIG⟧' in stem and used < len(refs):
            stem = stem.replace('⟦FIG⟧', refs[used], 1)
            used += 1
        stem = stem.replace('⟦FIG⟧', '')          # 图不够就抹掉占位符，别留怪字
        # 已经出现在正文/选项里的图不再重复贴一遍（2006_10 的 5 张图就在选项里）
        tail = [r for r in refs[used:] if r not in stem]
        if tail:
            stem = stem.rstrip() + '\n\n' + ' '.join(tail)
        cd = '%d年 AMC8' % q['year']
        data.append({
            'id': q['id'], 'tb': TB,
            'gg': '%d年' % q['year'], 'lv': 'AMC%d' % q['year'],   # 年级轴 = 年份（逐年）
            'cs': 2100 - q['year'],          # 年份倒序：新的排前面
            'cd': cd, 'src': cd, 's': cd,
            'n': str(no), 'q': stem, 'qe': qe,
            't': '选择题', 'd': _diff(no),
            'f': '', 'mf': [], 'p': [], 'k': [], 'im': ims,
            'pid': 'AMC8_%d' % q['year'],
        })

    # 节点给 **3 元**（不带 optgroup）：一年一节。年级轴（年份）倒序：新的排前面。
    years = sorted({d['gg'] for d in data}, key=lambda g: -int(g[:-1]))
    course_sub = OrderedDict()
    for g in years:
        nodes = OrderedDict()
        for x in data:
            if x['gg'] != g:
                continue
            nodes.setdefault(x['cs'], [x['cd'], 0])
            nodes[x['cs']][1] += 1
        course_sub[g] = [[s, nd[0], nd[1]] for s, nd in sorted(nodes.items())]

    FIG, skipped = {}, 0
    for i, (key, fp) in enumerate(sorted(fig_refs.items()), 1):
        try:
            d64, W, H = _fig_b64(fp)
            FIG[key] = {'d': d64, 'w': W, 'h': H}
        except Exception:
            skipped += 1
        if verbose and i % 100 == 0:
            print('  [AMC8] 配图 %d/%d' % (i, len(fig_refs)))

    # 嵌不进去的图不能悄悄消失
    n_hole = 0
    for x in data:
        miss = [k for k in x['im'] if k not in FIG]
        if not miss:
            continue
        for k in miss:
            x['q'] = x['q'].replace('![](asset://%s)' % k[3:], '⟦图缺失⟧')
        x['im'] = [k for k in x['im'] if k in FIG]
        n_hole += 1

    # 硬断言：占位符一个都不许漏出去（和 ⟦图缺失⟧ 那条自查同款写法）
    leak_fig = [x['id'] for x in data if '⟦FIG⟧' in x['q']]
    if leak_fig:
        raise SystemExit('✗ [AMC8] %d 题的题面还残留 ⟦FIG⟧：%s'
                         % (len(leak_fig), '、'.join(leak_fig[:5])))

    if verbose:
        print('  [AMC8] %d 个年份 / %d 套 / %d 题（%d 题中译、%d 题英文）；配图 %d 张'
              '（失败 %d → %d 题标⟦图缺失⟧），FIG %.1f MB；答案泄漏自查 %d 条'
              % (len(years), sum(len(v) for v in course_sub.values()), len(data),
                 n_zh, len(data) - n_zh, len(FIG), skipped, n_hole,
                 len(json.dumps(FIG)) / 1e6, leak))
    return data, course_sub, FIG


if __name__ == '__main__':
    d, c, f = load(os.path.dirname(os.path.abspath(__file__)))
    print('自测：', len(d), '题', len(c), '个年份', len(f), '图')
    if d:
        print('样例：', json.dumps({k: v for k, v in d[0].items() if k != 'q'},
                                   ensure_ascii=False))
        print('题面：', d[0]['q'][:220])
