#!/usr/bin/env python3
"""审计单元挑战多维表格中所有听力题型表的解析(explanation)字段"""

import requests
import json
import re
import sys
import copy

APP_ID = "cli_a931175d41799cc7"
APP_SECRET = "Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14"
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"

TABLES = {
    "听力-P1-图片选择题/选图题": "tbliZAhcc9C43B23",
    "听力-P2-表格填空题": "tblzTLNH7f13uWQN",
    "听力-P3-长对话选择": "tblgxsDn25oSq7WS",
    "听力-P4-短对话选择题": "tblVmeDtBDKsAEfz",
    "听力-P5-信息匹配题": "tblDssVmhGzc3UKd",
    "听力-P7-听力拖拽": "tbly9SvPEa44k3yX",
}

# === 问题检测模式 ===
# 选择题/阅读题特有用语（听力题不应出现）
CHOICE_READING_PATTERNS = [
    (r'干扰项', '含"干扰项"（选择题特有用语）'),
    (r'误选', '含"误选"（选择题特有用语）'),
    (r'所以选[ABCDEFGH]', '含"所以选X"格式（选择题用语）'),
    (r'正确答案是', '含"正确答案是"（过度直白）'),
    (r'如果有人[误错]?选', '含"如果有人错选"（选择题干扰分析）'),
    (r'如果[误错]?选', '含"如果误选"等假设用语'),
    (r'排除[法项]', '含"排除法/项"（选择题策略用语）'),
    (r'干扰[因素析]', '含干扰分析用语'),
    (r'容易混淆|容易选错|可能会选|不要选', '含混淆/选错提示（选择题口吻）'),
    (r'选[ABCDEFGH][^项择]', '含"选X"表述（选择题用语）'),
    (r'其他选项[^，。,.\n]{0,30}(没有|不是|错误|不对)', '含"其他选项没有/错误"（选择题干扰分析）'),
    (r'选项[ABCDEFGH][^，。,.\n]{0,20}(没有|不在|不是)', '含具体选项否定分析'),
    (r'虽然[^，。,.\n]{0,20}(接近|相似|类似)', '含"虽然…接近"（干扰项比较）'),
    (r'材料中[^，。,.\n]{0,10}(写到|明确说)', '含"材料中写到/明确说"（听力应说"音频中"）'),
    (r'原文中[^，。,.\n]{0,10}(写到|说)', '含"原文中写到"（应说"音频中"）'),
]

# 不匹配的能力标签
ABILITY_MISMATCH = [
    (r'写作能力|书面表达|写作技巧', '含写作能力标签（与听力题不匹配）'),
    (r'口语表达|发音准确|口语技巧', '含口语能力标签（与听力题不匹配）'),
    (r'拼写能力|拼写规则', '含拼写能力标签（与听力题不匹配）'),
    (r'语法结构分析|句式变换', '含语法分析标签（听力题应侧重理解）'),
    (r'阅读理解|阅读技巧|快速阅读', '含阅读能力标签（这是听力题）'),
]


def get_token():
    resp = requests.post(
        "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
        json={"app_id": APP_ID, "app_secret": APP_SECRET},
        timeout=30,
    )
    data = resp.json()
    return data["tenant_access_token"]


def get_all_records(token, table_id):
    """读取表内全部记录"""
    records = []
    page_token = None
    while True:
        url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{table_id}/records?page_size=500"
        if page_token:
            url += f"&page_token={page_token}"
        resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30)
        data = resp.json()
        if data.get("code") != 0:
            print(f"  ERROR reading: code={data.get('code')} msg={data.get('msg')}")
            break
        items = data.get("data", {}).get("items", [])
        records.extend(items)
        if not data.get("data", {}).get("has_more"):
            break
        page_token = data.get("data", {}).get("page_token")
    return records


def parse_json_data(fields):
    """从fields中解析jsonData"""
    jd = fields.get("jsonData")
    if not jd:
        return None
    if isinstance(jd, str):
        try:
            return json.loads(jd)
        except json.JSONDecodeError:
            return None
    return jd


def get_parts(json_data):
    """获取jsonData中的所有part键（first, second等）"""
    parts = []
    for key in ['first', 'second', 'third', 'fourth']:
        if key in json_data and isinstance(json_data[key], dict):
            parts.append(key)
    return parts


def get_explanations_from_question_set(question_set):
    """从questionSet中提取所有explanation"""
    results = []
    if not isinstance(question_set, list):
        return results
    for idx, q in enumerate(question_set):
        if not isinstance(q, dict):
            continue
        exp = q.get("explanation", "")
        if exp and isinstance(exp, str) and exp.strip():
            results.append({
                "index": idx,
                "explanation": exp.strip(),
                "question_text": q.get("question", "") or q.get("questionDesc", "") or "",
                "ability": q.get("ability", ""),
            })
    return results


def check_explanation(exp_text, question_text="", table_name=""):
    """检查单个explanation是否有问题"""
    if not exp_text or not exp_text.strip():
        return False, ""

    exp = exp_text.strip()

    # 1. 选择题/阅读题特有用语
    for pattern, desc in CHOICE_READING_PATTERNS:
        if re.search(pattern, exp):
            return True, desc

    # 2. 能力标签错配
    for pattern, desc in ABILITY_MISMATCH:
        if re.search(pattern, exp):
            return True, desc

    # 3. 纯能力标签描述（没有实际解析内容）
    # 如果explanation主要由能力标签组成，缺少音频信息分析
    if re.match(r'^[\s]*干扰抑制|显性事实|显性细节|隐性推理|多特征整合|信息筛选|听觉抓取', exp):
        # 检查是否有实际解析内容
        if len(exp) < 40 or not re.search(r'音频|听到|提到|说|描述|回答|对话|原文', exp):
            return True, "仅含能力标签，缺少音频信息分析的解析内容"

    return False, ""


def fix_explanation(exp_text, question_text="", question_index=0, table_name=""):
    """修复有问题的explanation"""
    if not exp_text or not isinstance(exp_text, str):
        return exp_text

    exp = exp_text.strip()

    # 移除选择题/阅读题特有用语
    exp = re.sub(r'干扰项[^，。,.\n]{0,40}[，。,.\n]?', '', exp)
    exp = re.sub(r'[，。]?\s*如果有人[误错]?选[^，。,.\n]{0,60}[，。,.\n]?', '', exp)
    exp = re.sub(r'[，。]?\s*如果[误错]?选[^，。,.\n]{0,60}[，。,.\n]?', '', exp)
    exp = re.sub(r'所以选[ABCDEFGH][，。]?', '', exp)
    exp = re.sub(r'正确答案是[^，。,.\n]{0,30}[，。,.\n]?', '', exp)
    exp = re.sub(r'排除[法项][^，。,.\n]{0,30}[，。,.\n]?', '', exp)
    exp = re.sub(r'干扰[因素析][^，。,.\n]{0,30}[，。,.\n]?', '', exp)
    exp = re.sub(r'容易混淆[^，。,.\n]{0,35}[，。,.\n]?', '', exp)
    exp = re.sub(r'可能会选错[^，。,.\n]{0,35}[，。,.\n]?', '', exp)
    exp = re.sub(r'其他选项[^，。,.\n]{0,40}(?:没有|不是|错误|不对)[^，。,.\n]{0,20}[，。,.\n]?', '', exp)
    exp = re.sub(r'选项[ABCDEFGH][^，。,.\n]{0,25}(?:没有|不在|不是)[^，。,.\n]{0,20}[，。,.\n]?', '', exp)
    exp = re.sub(r'虽然[^，。,.\n]{0,20}(?:接近|相似|类似)[^，。,.\n]{0,40}[，。,.\n]?', '', exp)
    
    # 替换"材料中…"→"音频中…"，"原文中…"→"音频中…"
    exp = re.sub(r'材料中([^，。,.\n]{0,10})(提到|写到|说)', r'音频中\1\2', exp)
    exp = re.sub(r'原文中([^，。,.\n]{0,10})(写到|说|提到)', r'音频中\1\2', exp)
    
    # 保留"答案是X"但更自然地改写
    exp = re.sub(r'答案是([ABCDEFGH])[，。]?', r'对应选项\1。', exp)
    exp = re.sub(r'答案是([^，。,.\n]{1,20})[，。]?', r'答案为\1。', exp)

    # 清理
    exp = re.sub(r'[，,]\s*[，,]', '，', exp)
    exp = re.sub(r'[。.]\s*[。.]', '。', exp)
    exp = re.sub(r'^\s*[，,]\s*', '', exp)
    exp = re.sub(r'\s+', '', exp).strip()

    # 如果清理后几乎为空，生成基础解析
    if not exp or len(exp) < 10:
        if question_text and question_text.strip():
            exp = f"根据音频中的关键信息进行判断，确认答案。"
        else:
            exp = f"根据音频内容理解，确认正确答案。"
    
    # 确保以句号结尾
    if exp and not exp.endswith(('。', '！', '？', '…')):
        exp += '。'

    return exp


def fix_p5_explanation(exp_text, table_name=""):
    """修复P5信息匹配题的整段explanation（¥¥分隔）"""
    if not exp_text or not isinstance(exp_text, str):
        return exp_text
    
    # 按¥¥分割
    segments = exp_text.split('¥¥')
    fixed_segments = []
    
    for seg in segments:
        seg = seg.strip()
        if not seg:
            continue
        
        # 同样的清理逻辑
        seg = re.sub(r'干扰项[^，。,.\n]{0,40}[，。,.\n]?', '', seg)
        seg = re.sub(r'[，。]?\s*如果有人[误错]?选[^，。,.\n]{0,60}[，。,.\n]?', '', seg)
        seg = re.sub(r'[，。]?\s*如果[误错]?选[^，。,.\n]{0,60}[，。,.\n]?', '', seg)
        seg = re.sub(r'所以选[ABCDEFGH][，。]?', '', seg)
        seg = re.sub(r'正确答案是[^，。,.\n]{0,30}[，。,.\n]?', '', seg)
        seg = re.sub(r'排除[法项][^，。,.\n]{0,30}[，。,.\n]?', '', seg)
        seg = re.sub(r'其他选项[^，。,.\n]{0,40}(?:没有|不是|错误|不对)[^，。,.\n]{0,20}[，。,.\n]?', '', seg)
        seg = re.sub(r'虽然[^，。,.\n]{0,20}(?:接近|相似|类似)[^，。,.\n]{0,40}[，。,.\n]?', '', seg)
        seg = re.sub(r'材料中([^，。,.\n]{0,10})(提到|写到|说)', r'音频中\1\2', seg)
        seg = re.sub(r'原文中([^，。,.\n]{0,10})(写到|说|提到)', r'音频中\1\2', seg)
        seg = re.sub(r'答案是([ABCDEFGH])[，。]?', r'对应选项\1。', seg)
        seg = re.sub(r'答案是([^，。,.\n]{1,20})[，。]?', r'答案为\1。', seg)
        seg = re.sub(r'\s+', '', seg).strip()
        seg = re.sub(r'[，,]\s*[，,]', '，', seg)
        seg = re.sub(r'^\s*[，,]\s*', '', seg)
        if seg and not seg.endswith(('。', '！', '？', '…')):
            seg += '。'
        if seg:
            fixed_segments.append(seg)
    
    return '¥¥'.join(fixed_segments)


def process_record(record, table_name, table_id, token):
    """处理单条记录，返回 (fixed_count, problem_count)"""
    record_id = record.get("record_id")
    fields = record.get("fields", {})
    
    json_data = parse_json_data(fields)
    if not json_data:
        return 0, 0, False
    
    parts = get_parts(json_data)
    if not parts:
        return 0, 0, False
    
    modified = False
    total_fixed = 0
    total_problems = 0
    
    for part_key in parts:
        part = json_data[part_key]
        part_type = part.get("type", "")
        
        # 检查 questionSet 中的 explanation（P1, P2, P3, P4, P7）
        if "questionSet" in part:
            question_set = part["questionSet"]
            if not isinstance(question_set, list):
                continue
            
            all_exps = get_explanations_from_question_set(question_set)
            
            # 检查所有explanation是否完全相同（2个及以上非空）
            non_empty = [e["explanation"] for e in all_exps if e["explanation"]]
            all_same = len(non_empty) > 1 and len(set(non_empty)) == 1
            
            for exp_info in all_exps:
                idx = exp_info["index"]
                exp = exp_info["explanation"]
                qt = exp_info["question_text"]
                
                has_problem, reason = check_explanation(exp, qt, table_name)
                
                if all_same and len(non_empty) > 1:
                    has_problem = True
                    if not reason:
                        reason = "同一部分内所有子题explanation完全相同"
                
                if has_problem:
                    total_problems += 1
                    fixed = fix_explanation(exp, qt, idx, table_name)
                    question_set[idx]["explanation"] = fixed
                    modified = True
                    total_fixed += 1
                    print(f"  [{table_name}] {part_key}.Q{idx+1}: {reason}")
                    print(f"    OLD: {exp[:120]}...")
                    print(f"    NEW: {fixed[:120]}...")
        
        # P5: explanation at part level
        if "explanation" in part and part_type == "listening_matchInfo":
            exp = part.get("explanation", "")
            if not exp or not isinstance(exp, str) or not exp.strip():
                continue
            
            has_problem, reason = check_explanation(exp, "", table_name)
            
            # Also check individual ¥¥-separated segments
            if not has_problem:
                segments = exp.split('¥¥')
                for seg in segments:
                    seg = seg.strip()
                    if not seg:
                        continue
                    sp, sr = check_explanation(seg, "", table_name)
                    if sp:
                        has_problem = True
                        reason = sr
                        break
            
            if has_problem:
                total_problems += 1
                fixed = fix_p5_explanation(exp, table_name)
                part["explanation"] = fixed
                modified = True
                total_fixed += len(exp.split('¥¥'))  # rough count
                print(f"  [{table_name}] {part_key}.explanation: {reason}")
                print(f"    OLD: {exp[:150]}...")
                print(f"    NEW: {fixed[:150]}...")
        
        # P7 also sometimes has explanation at top level covering all sub-answers
        if "explanation" in part and part_type == "listening_drag":
            exp = part.get("explanation", "")
            if not exp or not isinstance(exp, str) or not exp.strip():
                continue
            
            # P7's explanation might be in questionSet already (we checked above)
            # But some have it at part level too. Check if we already handled via questionSet
            # For now, if the part-level explanation is different from questionSet ones
            has_problem, reason = check_explanation(exp, "", table_name)
            
            if has_problem:
                total_problems += 1
                fixed = fix_explanation(exp, "", 0, table_name)
                part["explanation"] = fixed
                modified = True
                total_fixed += 1
                print(f"  [{table_name}] {part_key}.top-explanation: {reason}")
                print(f"    OLD: {exp[:150]}...")
                print(f"    NEW: {fixed[:150]}...")
    
    return total_fixed, total_problems, modified


def update_record(token, table_id, record_id, json_data, review_result="修复解析"):
    """更新记录"""
    json_str = json.dumps(json_data, ensure_ascii=False)
    update_body = {
        "fields": {
            "jsonData": json_str,
            "审校结果": review_result
        }
    }
    url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{table_id}/records/{record_id}"
    resp = requests.put(
        url,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json; charset=utf-8"
        },
        json=update_body,
        timeout=30,
    )
    result = resp.json()
    return result.get("code") == 0


def process_table(token, table_name, table_id):
    """处理单个表"""
    print(f"\n{'='*60}")
    print(f"表: {table_name} ({table_id})")
    print(f"{'='*60}")
    
    records = get_all_records(token, table_id)
    print(f"总记录数: {len(records)}")
    
    table_total_fixed = 0
    table_total_problems = 0
    table_total_updated = 0
    
    for record in records:
        record_id = record.get("record_id")
        fields = record.get("fields", {})
        
        json_data = parse_json_data(fields)
        if not json_data:
            continue
        
        # 深拷贝用于修改
        updated_data = copy.deepcopy(json_data)
        
        fixed, problems, modified = process_record_on_data(
            updated_data, table_name
        )
        
        table_total_fixed += fixed
        table_total_problems += problems
        
        if modified:
            success = update_record(token, table_id, record_id, updated_data)
            if success:
                table_total_updated += 1
            else:
                print(f"  FAIL: update failed for {record_id}")
    
    print(f"\n表统计: 有问题{table_total_problems}处, 修复{table_total_fixed}处, 更新{table_total_updated}条记录")
    
    return {
        "table_name": table_name,
        "total_records": len(records),
        "total_problems": table_total_problems,
        "total_fixed": table_total_fixed,
        "total_updated": table_total_updated,
    }


def process_record_on_data(json_data, table_name=""):
    """在json_data上执行修复，返回fix count"""
    parts = get_parts(json_data)
    if not parts:
        return 0, 0, False
    
    modified = False
    total_fixed = 0
    total_problems = 0
    
    for part_key in parts:
        part = json_data[part_key]
        part_type = part.get("type", "")
        
        if "questionSet" in part:
            question_set = part["questionSet"]
            if not isinstance(question_set, list):
                continue
            
            all_exps = get_explanations_from_question_set(question_set)
            non_empty = [e["explanation"] for e in all_exps if e["explanation"]]
            all_same = len(non_empty) > 1 and len(set(non_empty)) == 1
            
            for exp_info in all_exps:
                idx = exp_info["index"]
                exp = exp_info["explanation"]
                qt = exp_info["question_text"]
                
                has_problem, reason = check_explanation(exp, qt, table_name)
                
                if all_same and len(non_empty) > 1:
                    has_problem = True
                    reason = "同一部分内所有子题explanation完全相同"
                
                if has_problem:
                    total_problems += 1
                    fixed = fix_explanation(exp, qt, idx, table_name)
                    question_set[idx]["explanation"] = fixed
                    modified = True
                    total_fixed += 1
                    print(f"  [{table_name}] {part_key}.Q{idx+1}: {reason}")
                    print(f"    OLD: {exp[:120]}...")
                    print(f"    NEW: {fixed[:120]}...")
        
        if "explanation" in part and part_type in ("listening_matchInfo", "listening_drag"):
            exp = part.get("explanation", "")
            if not exp or not isinstance(exp, str) or not exp.strip():
                continue
            
            has_problem, reason = False, ""
            
            if part_type == "listening_matchInfo":
                segments = exp.split('¥¥')
                for seg in segments:
                    seg = seg.strip()
                    if not seg:
                        continue
                    sp, sr = check_explanation(seg, "", table_name)
                    if sp:
                        has_problem = True
                        reason = sr
                        break
            else:
                has_problem, reason = check_explanation(exp, "", table_name)
            
            if has_problem:
                total_problems += 1
                if part_type == "listening_matchInfo":
                    fixed = fix_p5_explanation(exp, table_name)
                else:
                    fixed = fix_explanation(exp, "", 0, table_name)
                part["explanation"] = fixed
                modified = True
                total_fixed += 1
                print(f"  [{table_name}] {part_key}.explanation: {reason}")
                print(f"    OLD: {exp[:150]}...")
                print(f"    NEW: {fixed[:150]}...")
    
    return total_fixed, total_problems, modified


def main():
    print("=" * 60)
    print("听力题型解析(explanation)字段审计与修复")
    print("=" * 60)
    
    token = get_token()
    print(f"Token: {token[:15]}...")
    
    all_results = []
    
    for table_name, table_id in TABLES.items():
        result = process_table(token, table_name, table_id)
        all_results.append(result)
    
    # 汇总
    print("\n\n" + "=" * 70)
    print("审计汇总")
    print("=" * 70)
    
    total_problems = sum(r["total_problems"] for r in all_results)
    total_fixed = sum(r["total_fixed"] for r in all_results)
    total_updated = sum(r["total_updated"] for r in all_results)
    
    print(f"\n{'表名':<30} {'记录':>5} {'有问题':>6} {'修复':>6} {'更新记录':>8}")
    print("-" * 65)
    for r in all_results:
        print(f"{r['table_name']:<30} {r['total_records']:>5} {r['total_problems']:>6} {r['total_fixed']:>6} {r['total_updated']:>8}")
    print("-" * 65)
    print(f"{'合计':<30} {'':>5} {total_problems:>6} {total_fixed:>6} {total_updated:>8}")
    
    # 保存结果
    output = {
        "summary": {
            "total_problems": total_problems,
            "total_fixed": total_fixed,
            "total_updated": total_updated,
        },
        "tables": all_results,
    }
    with open("/root/.openclaw/workspace-xiaoyan/output/audit_listening_explanation_results.json", "w", encoding="utf-8") as f:
        json.dump(output, f, ensure_ascii=False, indent=2)
    print(f"\n详细结果已保存到 output/audit_listening_explanation_results.json")


if __name__ == "__main__":
    main()
