#!/usr/bin/env python3
"""合作阅读 S0 表审校脚本 — 纯规则检查，不调用大模型"""
import json, sys, requests, re

TOKEN = None
APP_TOKEN = 'HIuLbJ6E8aNw2lsdT1YcvNOmnog'
TABLE_ID = 'tblAZT1KNUuNY3cN'

def get_token():
    global TOKEN
    if TOKEN:
        return TOKEN
    r = requests.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
                      json={'app_id':'cli_a931175d41799cc7','app_secret':'Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14'})
    TOKEN = r.json()['tenant_access_token']
    return TOKEN

def fetch_all(token):
    records = []
    page_token = ''
    while True:
        r = requests.get(
            f'https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records?page_size=100',
            headers={'Authorization': f'Bearer {token}'},
            params={'page_token': page_token})
        d = r.json()
        items = d.get('data',{}).get('items',[])
        records.extend(items)
        if not d.get('data',{}).get('has_more'):
            break
        page_token = d.get('data',{}).get('page_token','')
    return records

def safe_json(fields, key):
    v = fields.get(key, '')
    if not v:
        return None, 'EMPTY'
    try:
        return json.loads(v), 'OK'
    except Exception as e:
        return None, f'PARSE_ERROR: {e}'

def audit(record):
    fields = record['fields']
    fid = (fields.get('ID') or '').strip()
    rid = record['record_id']
    issues = []
    warnings = []

    # ===== 1. ID 基础检查 =====
    if not fid:
        issues.append('❌ ID字段为空')
    elif fid == 'None' or fid == 'null':
        issues.append('❌ ID字段异常值(None/null)')
    elif not fid.isdigit():
        issues.append(f'❌ ID字段非纯数字: "{fid}"')

    # ===== 2. JSON 字段解析检查 =====
    json_fields = {
        'taskData': fields.get('taskData',''),
        'sequenceData': fields.get('sequenceData',''),
        'textData': fields.get('textData',''),
        'learningData': fields.get('learningData',''),
        'kpInfo': fields.get('kpInfo',''),
    }
    parsed = {}
    for key, raw in json_fields.items():
        obj, status = safe_json(fields, key)
        parsed[key] = obj
        if status != 'OK' and raw:  # 有内容但解析失败
            issues.append(f'❌ {key} JSON解析失败: {status}')
        elif status == 'EMPTY':
            warnings.append(f'⚠️ {key} 为空')

    # ===== 3. ID 一致性检查 =====
    td = parsed.get('taskData')
    kp = parsed.get('kpInfo')
    if td and kp:
        if td.get('cId','') != fid:
            issues.append(f'❌ taskData.cId="{td.get("cId")}" ≠ ID="{fid}"')
        if kp.get('cId','') != fid:
            issues.append(f'❌ kpInfo.cId="{kp.get("cId")}" ≠ ID="{fid}"')
        if td.get('cId','') != kp.get('cId',''):
            issues.append(f'❌ taskData.cId≠kpInfo.cId ({td.get("cId")} vs {kp.get("cId")})')

    # ===== 4. textData 检查 =====
    td_obj = parsed.get('textData')
    if td_obj:
        texts = td_obj.get('text', [])
        questions = td_obj.get('question', [])

        for qi, q in enumerate(questions):
            # 4a. meaning 不为空
            meaning = q.get('meaning','')
            if not meaning:
                issues.append(f'❌ textData.question[{qi}] meaning 为空')

            # 4b. answer 在范围内
            answer = q.get('answer', [])
            opts = q.get('optionList', [])
            if isinstance(answer, list) and len(answer) > 0:
                aidx = answer[0]
                if aidx >= len(opts):
                    issues.append(f'❌ textData.question[{qi}] answer={aidx} 超出optionList范围({len(opts)})')
            elif not answer:
                issues.append(f'❌ textData.question[{qi}] answer 为空')

            # 4c. optionList 每项有 feedback
            for oi, opt in enumerate(opts):
                fb = opt.get('feedback', {})
                if not fb.get('content', ''):
                    issues.append(f'❌ textData.question[{qi}].option[{oi}] feedback 为空')

            # 4d. npcName 非空
            if not q.get('npcName',''):
                issues.append(f'❌ textData.question[{qi}] npcName 为空')

        # 4e. text 内容非空
        for ti, t in enumerate(texts):
            if not t.get('textBody',''):
                issues.append(f'❌ textData.text[{ti}] textBody 为空')
            if not t.get('textTitle',''):
                issues.append(f'⚠️ textData.text[{ti}] textTitle 为空')

    # ===== 5. learningData 检查 =====
    ld = parsed.get('learningData')
    if ld:
        parts = ld.get('learningPart', [])
        for pi, part in enumerate(parts):
            learnings = part.get('learning', [])
            for li, lq in enumerate(learnings):
                # 5a. meaning 不为空
                if not lq.get('meaning',''):
                    issues.append(f'❌ learningData.learningPart[{pi}].learning[{li}] meaning 为空')

                # 5b. answer 在范围内
                answer = lq.get('answer', [])
                opts = lq.get('optionList', [])
                if isinstance(answer, list) and len(answer) > 0:
                    aidx = answer[0]
                    if aidx >= len(opts):
                        issues.append(f'❌ learningPart[{pi}].learning[{li}] answer={aidx} 超出optionList({len(opts)})')

                # 5c. optionList 每项有 feedback
                for oi, opt in enumerate(opts):
                    fb = opt.get('feedback', {})
                    if not fb.get('content', ''):
                        issues.append(f'❌ learningPart[{pi}].learning[{li}].option[{oi}] feedback 为空')

            # 5d. leadIn 检查
            lid = part.get('leadIn', {})
            if lid and not lid.get('desc',''):
                issues.append(f'❌ learningPart[{pi}].leadIn.desc 为空')

    # ===== 6. sequenceData 索引越界检查 =====
    sd = parsed.get('sequenceData')
    if sd and td_obj:
        seq = sd.get('sequence', [])
        texts = td_obj.get('text', [])
        questions = td_obj.get('question', [])
        for si, step in enumerate(seq):
            if 'text' in step:
                idx = step['text'][0] if step['text'] else -1
                if idx >= len(texts):
                    issues.append(f'❌ sequence[{si}].text[{idx}] 越界 (text共{len(texts)}段)')
            if 'question' in step:
                idx = step['question'][0] if step['question'] else -1
                if idx >= len(questions):
                    issues.append(f'❌ sequence[{si}].question[{idx}] 越界 (question共{len(questions)}题)')

    # ===== 7. NPC 一致性 =====
    npc_config = fields.get('角色配置', '')
    npc_names_from_config = set()
    if npc_config and npc_config != 'None':
        # 格式: "Skylar：308" 或 "Brainy" 等
        for m in re.findall(r'([A-Za-z#\d]+)', npc_config):
            npc_names_from_config.add(m)

    # 收集所有实际使用的 npcName
    used_npc = set()
    if td_obj:
        for q in td_obj.get('question', []):
            nm = q.get('npcName','')
            if nm:
                used_npc.add(nm.split('#')[0])  # 去掉 #编号

    if ld:
        for part in ld.get('learningPart', []):
            nm = part.get('npcName','')
            if nm:
                used_npc.add(nm.split('#')[0])
        closing = ld.get('closing', {})
        if closing.get('npcName'):
            used_npc.add(closing['npcName'].split('#')[0])

    # 不在配置中的 NPC
    for nm in used_npc:
        if nm not in npc_names_from_config and npc_names_from_config:
            warnings.append(f'⚠️ NPC "{nm}" 出现在数据中但未在角色配置中声明')

    # ===== 8. kpInfo 与 taskData.key 对应性 =====
    if td and kp:
        keys = set()
        for group in td.get('key', []):
            for item in group.get('keyList', []):
                keys.add(item.get('content',''))

        kp_words = set()
        for info in kp.get('kpInfo', []):
            kp_words.add(info.get('kpTitle',''))

        only_in_key = keys - kp_words
        only_in_kp = kp_words - keys
        if only_in_key:
            warnings.append(f'⚠️ taskData.key 有但 kpInfo 无: {only_in_key}')
        if only_in_kp:
            warnings.append(f'⚠️ kpInfo 有但 taskData.key 无: {only_in_kp}')

    # ===== 9. 能力维度覆盖检查 =====
    if td_obj:
        meanings = set()
        for q in td_obj.get('question', []):
            m = q.get('meaning','')
            if m:
                meanings.add(m)
        known = {'理解文章中的细节信息','理解文章的主旨大意','推断文章的隐含意义','理解文章中的指代关系'}
        unknown = meanings - known
        if unknown:
            warnings.append(f'⚠️ textData.question 发现未知meaning标签: {unknown}')
        # 检查是否覆盖了至少 Detail + Main idea
        if '理解文章中的细节信息' not in meanings:
            warnings.append('⚠️ 缺少「理解文章中的细节信息」维度')
        if '理解文章的主旨大意' not in meanings:
            warnings.append('⚠️ 缺少「理解文章的主旨大意」维度')

    return {
        'id': fid,
        'record_id': rid,
        'title': td.get('title','?') if td else '?',
        'issues': issues,
        'warnings': warnings,
        'total_severity': len(issues),
        'total_warnings': len(warnings)
    }


if __name__ == '__main__':
    token = get_token()
    records = fetch_all(token)
    results = [audit(r) for r in records]

    # 输出
    total_issues = 0
    total_warnings = 0
    for r in results:
        sev = '🔴' if r['total_severity'] > 0 else ('🟡' if r['total_warnings'] > 0 else '🟢')
        print(f"\n{'='*60}")
        print(f"{sev} [{r['id']}] {r['title']}")
        print(f"{'='*60}")
        for i in r['issues']:
            print(f"  {i}")
            total_issues += 1
        for w in r['warnings']:
            print(f"  {w}")
            total_warnings += 1
        if not r['issues'] and not r['warnings']:
            print(f"  ✅ 通过")

    print(f"\n{'='*60}")
    print(f"汇总: {len(results)} 条记录 | 🔴 {total_issues} 个错误 | ⚠️ {total_warnings} 个警告")
    zero = sum(1 for r in results if r['total_severity']==0 and r['total_warnings']==0)
    print(f"全通过: {zero}/{len(results)}")

    # 输出 JSON 供后续使用
    print('\n---RESULT_JSON---')
    print(json.dumps(results, ensure_ascii=False, indent=2))
