#!/usr/bin/env python3
"""
互动组件配置格式校验脚本
覆盖全部 27 种中互动 + 15 种核心互动组件类型
校验维度：通用规则 + 题型专属规则 + 拼写检查

用法:
  python3 validate_format.py <teaching_config_text> --type <cn_type>
  python3 validate_format.py --file <path>                          # 批量校验
  python3 validate_format.py --sheet <spreadsheet_token> <sheet_id> # 飞书Sheet校验
"""

import re
import sys
import json
import argparse
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from enum import Enum


# ============================================================
# 第一部分：题型格式特征定义（唯一真相源）
# ============================================================

class ComponentCategory(Enum):
    MID = "mid"      # 中互动
    CORE = "core"    # 核心互动


@dataclass
class FormatSpec:
    """单个题型的格式规范"""
    cn_name: str
    cType: str
    category: ComponentCategory

    # 题型特征标记
    has_blank_marker: bool = False       # 题干必须有 ___
    has_options: bool = False            # 有选项列表
    has_answer: bool = False             # 有答案
    has_per_option_feedback: bool = False # 每个选项有独立反馈
    is_read_type: bool = False           # 朗读型
    is_express_type: bool = False        # 表达型
    is_sentence_build: bool = False      # 组句型（选项为碎片，答案为拼接句）
    is_fillin: bool = False              # 挖空/填空型

    # 文本规范
    question_lang: str = ""              # 题干语言要求: "en" / "cn" / ""
    option_format: str = ""              # 选项格式: "word" / "fragment" / "sentence" / "letter" / ""
    answer_format: str = ""              # 答案格式: "word" / "sentence" / "index" / ""

    # 附加说明
    notes: str = ""


# ============================================================
# 中互动组件格式规范（27种）
# ============================================================

MID_FORMAT_SPECS: Dict[str, FormatSpec] = {
    "对话互动": FormatSpec(
        cn_name="对话互动", cType="mid_sentence_dialogue",
        category=ComponentCategory.MID,
        is_read_type=True,
        question_lang="en",
        notes="朗读型，无选项无答案"),
    "对话朗读": FormatSpec(
        cn_name="对话朗读", cType="mid_dialog_repeat",
        category=ComponentCategory.MID,
        is_read_type=True,
        question_lang="en",
        notes="朗读型，无选项无答案"),
    "对话表达": FormatSpec(
        cn_name="对话表达", cType="mid_dialog_express",
        category=ComponentCategory.MID,
        is_express_type=True,
        notes="表达型，有示例回答，无选项"),
    "对话选读": FormatSpec(
        cn_name="对话选读", cType="mid_dialog_select",
        category=ComponentCategory.MID,
        has_options=True,
        has_per_option_feedback=True,
        option_format="sentence",
        notes="选项为完整句子，每个选项有独立反馈"),
    "对话选择": FormatSpec(
        cn_name="对话选择", cType="mid_dialog_choose",
        category=ComponentCategory.MID,
        has_options=True,
        has_per_option_feedback=True,
        option_format="sentence",
        notes="选项为完整回复句，每个选项有独立反馈，选项需标注（音频）"),
    "对话挖空": FormatSpec(
        cn_name="对话挖空", cType="mid_dialog_fillin",
        category=ComponentCategory.MID,
        has_blank_marker=True,
        has_options=True,
        is_fillin=True,
        question_lang="en",
        option_format="word",
        notes="题干必须是带 ___ 的英文句子，选项为填入空位的单词"),
    "对话组句": FormatSpec(
        cn_name="对话组句", cType="mid_dialog_sentence",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        is_sentence_build=True,
        option_format="fragment",
        answer_format="sentence",
        notes="选项为句子片段，答案为拼接后的完整句"),
    "信息描写": FormatSpec(
        cn_name="信息描写", cType="mid_message_trace",
        category=ComponentCategory.MID,
        has_answer=True,
        answer_format="word",
        notes="答案为单词/短语"),
    "信息拼词": FormatSpec(
        cn_name="信息拼词", cType="mid_message_spell",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        option_format="letter",
        answer_format="word",
        notes="选项为字母，答案为拼成的单词"),
    "信息组句": FormatSpec(
        cn_name="信息组句", cType="mid_message_combine",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        is_sentence_build=True,
        option_format="fragment",
        answer_format="sentence",
        notes="选项为句子片段，答案为拼接后的完整句"),
    "信息补词": FormatSpec(
        cn_name="信息补词", cType="mid_message_fillin",
        category=ComponentCategory.MID,
        has_answer=True,
        answer_format="word",
        notes="答案为单词"),
    "信息填词": FormatSpec(
        cn_name="信息填词", cType="mid_message_word",
        category=ComponentCategory.MID,
        has_answer=True,
        answer_format="word",
        notes="答案为单词/短语"),
    "信息填句": FormatSpec(
        cn_name="信息填句", cType="mid_message_sentence",
        category=ComponentCategory.MID,
        has_answer=True,
        answer_format="sentence",
        notes="答案为完整句子"),
    "物品互动": FormatSpec(
        cn_name="物品互动", cType="mid_vocab_item",
        category=ComponentCategory.MID,
        has_options=True,
        option_format="word",
        notes="选项为单词"),
    "图片互动": FormatSpec(
        cn_name="图片互动", cType="mid_vocab_image",
        category=ComponentCategory.MID,
        has_answer=True,
        notes="答案为坐标/区域标识"),
    "图片单选": FormatSpec(
        cn_name="图片单选", cType="mid_image_choose",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        answer_format="index",
        notes="答案为选项索引"),
    "图片多选": FormatSpec(
        cn_name="图片多选", cType="mid_image_multiple",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        answer_format="index",
        notes="答案为选项索引列表"),
    "图片有序": FormatSpec(
        cn_name="图片有序", cType="mid_image_sequence",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        answer_format="index",
        notes="答案为排序索引"),
    "图片拖拽": FormatSpec(
        cn_name="图片拖拽", cType="mid_image_drag",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        answer_format="index",
        notes="答案为拖拽目标索引"),
    "材料互动": FormatSpec(
        cn_name="材料互动", cType="mid_sentence_material",
        category=ComponentCategory.MID,
        has_options=True,
        notes="选项为材料/句子"),
    "指令互动": FormatSpec(
        cn_name="指令互动", cType="mid_vocab_instruction",
        category=ComponentCategory.MID,
        has_options=True,
        notes="选项为指令"),
    "挖空互动": FormatSpec(
        cn_name="挖空互动", cType="mid_grammar_cloze",
        category=ComponentCategory.MID,
        has_blank_marker=True,
        has_options=True,
        has_answer=True,
        is_fillin=True,
        question_lang="en",
        option_format="word",
        notes="题干必须是带 ___ 的英文句子，选项为填入空位的单词"),
    "组句互动": FormatSpec(
        cn_name="组句互动", cType="mid_grammar_sentence",
        category=ComponentCategory.MID,
        has_options=True,
        has_answer=True,
        is_sentence_build=True,
        option_format="fragment",
        answer_format="sentence",
        notes="选项为句子片段，答案为拼接后的完整句"),
    "填词互动": FormatSpec(
        cn_name="填词互动", cType="mid_vocab_fillBlank",
        category=ComponentCategory.MID,
        has_answer=True,
        answer_format="word",
        notes="答案为单词"),
    "语音互动": FormatSpec(
        cn_name="语音互动", cType="mid_sentence_voice",
        category=ComponentCategory.MID,
        has_options=True,
        notes="语音类选项"),
    "造句互动": FormatSpec(
        cn_name="造句互动", cType="mid_sentence_makeSentence",
        category=ComponentCategory.MID,
        is_express_type=True,
        notes="造句型，自由输出"),
    "发音互动": FormatSpec(
        cn_name="发音互动", cType="mid_pron_pron",
        category=ComponentCategory.MID,
        is_read_type=True,
        notes="发音型，朗读输出"),
}


# ============================================================
# 核心互动组件格式规范（15种）
# ============================================================

CORE_FORMAT_SPECS: Dict[str, FormatSpec] = {
    "合作阅读": FormatSpec(
        cn_name="合作阅读", cType="core_reading_order",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "看图选词": FormatSpec(
        cn_name="看图选词", cType="core_reading_imageDrag",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "口语快答": FormatSpec(
        cn_name="口语快答", cType="core_speaking_reply",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "口语独白": FormatSpec(
        cn_name="口语独白", cType="core_speaking_monologue",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "口语妙问": FormatSpec(
        cn_name="口语妙问", cType="core_speaking_inquiry",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "看图说话": FormatSpec(
        cn_name="看图说话", cType="core_speaking_image",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "口语探讨": FormatSpec(
        cn_name="口语探讨", cType="core_speaking_discuss",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "合作听力": FormatSpec(
        cn_name="合作听力", cType="core_listening_order",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "听力拖拽": FormatSpec(
        cn_name="听力拖拽", cType="core_listening_drag",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "听力选择": FormatSpec(
        cn_name="听力选择", cType="core_listening_choose",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "写作互动": FormatSpec(
        cn_name="写作互动", cType="core_writing_questionMakeSentence",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "看图组句": FormatSpec(
        cn_name="看图组句", cType="core_writing_imgMakeSentence",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "看图撰写": FormatSpec(
        cn_name="看图撰写", cType="core_writing_imgWrite",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "邮件组句": FormatSpec(
        cn_name="邮件组句", cType="core_writing_questionMakeSentence",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
    "邮件撰写": FormatSpec(
        cn_name="邮件撰写", cType="core_writing_questionWrite",
        category=ComponentCategory.CORE,
        notes="核心互动，JSON结构见 core-content-json-standard"),
}


# 合并所有格式规范
ALL_FORMAT_SPECS = {**MID_FORMAT_SPECS, **CORE_FORMAT_SPECS}

# 配图后缀变体映射（剥离后缀后查主表）
PICTURE_VARIANTS = {
    "对话朗读-配图": "对话朗读",
    "对话表达-配图": "对话表达",
    "对话选读-配图": "对话选读",
    "对话选择-配图": "对话选择",
    "对话挖空-配图": "对话挖空",
    "对话组句-配图": "对话组句",
    "图片单选-配图": "图片单选",
}


# ============================================================
# 第二部分：通用校验规则
# ============================================================

class Severity(Enum):
    ERROR = "🔴"    # 必须修复
    WARN = "🟡"     # 建议修复
    INFO = "⚪"     # 提示


@dataclass
class Violation:
    severity: Severity
    rule: str
    detail: str
    location: str = ""


def resolve_type(cn_type: str) -> Tuple[Optional[FormatSpec], str]:
    """解析组件类型名，处理配图后缀变体"""
    cn_type = cn_type.strip()
    # 精确匹配
    if cn_type in ALL_FORMAT_SPECS:
        return ALL_FORMAT_SPECS[cn_type], cn_type
    # 配图变体
    if cn_type in PICTURE_VARIANTS:
        base = PICTURE_VARIANTS[cn_type]
        return ALL_FORMAT_SPECS.get(base), cn_type
    # 模糊匹配（去除末尾数字）
    m = re.match(r'^(.+?)\d*$', cn_type)
    if m:
        base = m.group(1).strip()
        if base in ALL_FORMAT_SPECS:
            return ALL_FORMAT_SPECS[base], cn_type
    return None, cn_type


# ============================================================
# 通用规则检查
# ============================================================

def check_title_spacing(text: str) -> List[Violation]:
    """检查标题中中英文混排空格"""
    violations = []
    for line in text.split('\n'):
        if '【任务标题】' in line:
            title = line.replace('【任务标题】', '').strip()
            # 中文字符后紧跟英文字母（缺空格）
            issues = re.finditer(r'([\u4e00-\u9fff])([A-Za-z])', title)
            for m in issues:
                violations.append(Violation(
                    Severity.ERROR,
                    "中英文混排缺空格",
                    f"标题'{title}'中'{m.group(1)}{m.group(2)}'之间应加半角空格",
                    f"【任务标题】"
                ))
            # 英文字母后紧跟中文字符（缺空格）
            issues = re.finditer(r'([A-Za-z])([\u4e00-\u9fff])', title)
            for m in issues:
                violations.append(Violation(
                    Severity.ERROR,
                    "中英文混排缺空格",
                    f"标题'{title}'中'{m.group(1)}{m.group(2)}'之间应加半角空格",
                    f"【任务标题】"
                ))
    return violations


def check_feedback_colon(text: str) -> List[Violation]:
    """检查反馈中角色名冒号格式：应为 '角色名: 台词'，冒号前无空格，冒号为英文半角"""
    violations = []
    # 匹配反馈行中的 "角色名 : 台词"（冒号前有空格）
    pattern_space = re.compile(r'([A-Z][a-zA-Z]*)\s+:\s')
    for m in pattern_space.finditer(text):
        name = m.group(1)
        violations.append(Violation(
            Severity.ERROR,
            "反馈冒号前多余空格",
            f"'{name} :' 应改为 '{name}:'",
            "【互动反馈】"
        ))
    # 匹配中文冒号
    pattern_cn_colon = re.compile(r'([A-Z][a-zA-Z]*)\s*：')
    for m in pattern_cn_colon.finditer(text):
        name = m.group(1)
        violations.append(Violation(
            Severity.ERROR,
            "反馈冒号使用了中文冒号",
            f"'{name}：' 应改为 '{name}:'",
            "【互动反馈】"
        ))
    return violations


def check_markdown(text: str) -> List[Violation]:
    """检查是否使用了 Markdown 标记"""
    violations = []
    # 检查加粗/斜体，排除 ___（挖空标记）
    # ** 和 __ 是加粗，* 和 _ 是斜体
    if '**' in text:
        violations.append(Violation(
            Severity.ERROR,
            "禁止使用 Markdown 加粗标记",
            "发现 '**' 标记",
            ""
        ))
    # __ 检查：排除 ___（连续三个下划线是挖空标记）
    # 检查是否有 __ 但不是 ___ 的一部分
    stripped = text.replace('___', '')  # 移除挖空标记
    if '__' in stripped:
        violations.append(Violation(
            Severity.ERROR,
            "禁止使用 Markdown 加粗标记",
            "发现 '__' 标记（非挖空标记 ___）",
            ""
        ))
    # * 检查：排除 **（已单独检查）
    if re.search(r'(?<!\*)\*(?!\*)', text):
        violations.append(Violation(
            Severity.ERROR,
            "禁止使用 Markdown 斜体标记",
            "发现 '*' 标记",
            ""
        ))
    # _ 检查：排除 ___ 和 __
    single_underscore = re.sub(r'_{2,}', '', text)  # 移除连续2个以上的下划线
    if '_' in single_underscore:
        violations.append(Violation(
            Severity.ERROR,
            "禁止使用 Markdown 斜体标记",
            "发现 '_' 标记（非挖空标记）",
            ""
        ))
    return violations


def check_spelling(text: str) -> List[Violation]:
    """检查常见拼写错误"""
    violations = []
    common_errors = {
        'shoud': 'should',
        'becasue': 'because',
        'thier': 'their',
        'recieve': 'receive',
        'teh': 'the',
        'adn': 'and',
        'writting': 'writing',
        'lisence': 'license',
        'occured': 'occurred',
        'untill': 'until',
    }
    words = re.findall(r'\b\w+\b', text)
    for word in words:
        lower = word.lower()
        if lower in common_errors:
            violations.append(Violation(
                Severity.ERROR,
                "拼写错误",
                f"'{word}' 应为 '{common_errors[lower]}'",
                ""
            ))
    return violations


def check_punctuation(text: str) -> List[Violation]:
    """检查标点符号规范"""
    violations = []
    # 英文内容中的中文标点
    # 在【互动内容】区域检查
    content_match = re.search(r'【互动内容】\n(.*?)(?=【|$)', text, re.DOTALL)
    if content_match:
        content = content_match.group(1)
        # 检查英文句子中的中文标点
        for cn_punct, en_punct in [('，', ','), ('。', '.'), ('！', '!'), ('？', '?'), ('：', ':')]:
            if cn_punct in content:
                # 只报告在英文语境中的（简单判断：前后有英文字母）
                for m in re.finditer(rf'[A-Za-z]\s*{re.escape(cn_punct)}\s*[A-Za-z]', content):
                    violations.append(Violation(
                        Severity.ERROR,
                        "英文内容中使用了中文标点",
                        f"'{cn_punct}' 应改为 '{en_punct}'",
                        "【互动内容】"
                    ))
    return violations


# ============================================================
# 题型专属规则检查
# ============================================================

def check_blank_marker(text: str, spec: FormatSpec) -> List[Violation]:
    """挖空类题型：检查题干是否包含 ___ 标记"""
    violations = []
    if not spec.has_blank_marker:
        return violations

    content_match = re.search(r'【互动内容】\n(.*?)(?=【|$)', text, re.DOTALL)
    if content_match:
        content = content_match.group(1)
        if '___' not in content:
            violations.append(Violation(
                Severity.ERROR,
                f"{spec.cn_name} 题干缺少 ___ 挖空标记",
                "题干必须包含 '___' 表示需要填空的位置。"
                "正确格式示例：'He doesn't ___ to tell us!'",
                "【互动内容】"
            ))
    return violations


def check_question_language(text: str, spec: FormatSpec) -> List[Violation]:
    """检查题干语言是否符合要求"""
    violations = []
    if not spec.question_lang:
        return violations

    content_match = re.search(r'【互动内容】\n(.*?)(?=【|$)', text, re.DOTALL)
    if not content_match:
        return violations

    content = content_match.group(1)
    # 取第一行非空、非"要求："、非"题目："的内容作为题干
    lines = [l.strip() for l in content.split('\n') if l.strip()
             and not l.strip().startswith('要求')
             and not l.strip().startswith('题目')
             and not l.strip().startswith('选项')
             and not l.strip().startswith('答案')
             and not l.strip().startswith('辅助')]

    if spec.question_lang == "en" and lines:
        first_line = lines[0]
        # 检查是否以中文为主（超过50%中文字符）
        cn_chars = len(re.findall(r'[\u4e00-\u9fff]', first_line))
        total_chars = len(first_line.replace(' ', ''))
        if total_chars > 0 and cn_chars / total_chars > 0.5:
            violations.append(Violation(
                Severity.ERROR,
                f"{spec.cn_name} 题干应为英文",
                f"当前题干以中文为主: '{first_line[:60]}...'",
                "【互动内容】"
            ))
    return violations


def check_options_format(text: str, spec: FormatSpec) -> List[Violation]:
    """检查选项格式"""
    violations = []
    if not spec.has_options:
        return violations

    # 提取选项
    options = re.findall(r'选项\d+[：:]\s*(.+)', text)
    if not options:
        # 可能是"选项："后跟列表的格式
        opt_match = re.search(r'选项[：:]\s*\n?(.*?)(?=答案|辅助|【|$)', text, re.DOTALL)
        if opt_match:
            options = [o.strip() for o in opt_match.group(1).split('\n') if o.strip()]

    if spec.has_per_option_feedback:
        # 对话选择/对话选读：每个选项必须有反馈
        feedback_count = len(re.findall(r'反馈\s*\w+\s*:', text))
        if feedback_count < len(options):
            violations.append(Violation(
                Severity.ERROR,
                f"{spec.cn_name} 选项缺少逐项反馈",
                f"有 {len(options)} 个选项但只有 {feedback_count} 条反馈",
                "【互动内容】"
            ))

    if spec.is_sentence_build:
        # 组句型：检查是否有"答案"行
        if '答案' not in text:
            violations.append(Violation(
                Severity.ERROR,
                f"{spec.cn_name} 缺少答案行",
                "组句型必须有'答案：'行，给出拼接后的完整句子",
                "【互动内容】"
            ))
        # 选项应为片段，答案应为完整句
        answer_match = re.search(r'答案[：:]\s*(.+)', text)
        if answer_match and options:
            answer = answer_match.group(1).strip()
            # 简单检查：答案应该比单个选项长
            max_opt_len = max(len(o) for o in options)
            if len(answer) <= max_opt_len * 1.5:
                violations.append(Violation(
                    Severity.WARN,
                    f"{spec.cn_name} 答案可能不是完整拼接句",
                    f"答案'{answer}'长度与选项片段接近",
                    "【互动内容】"
                ))

    return violations


def check_audio_marker(text: str, spec: FormatSpec) -> List[Violation]:
    """检查需要音频标记的题型"""
    violations = []
    # 对话选择、对话选读、对话挖空、对话组句：选项/题干后应有（音频）
    audio_types = {"对话选择", "对话选读", "对话挖空", "对话组句"}
    if spec.cn_name not in audio_types:
        return violations

    content_match = re.search(r'【互动内容】\n(.*?)(?=【|$)', text, re.DOTALL)
    if content_match:
        content = content_match.group(1)
        # 检查是否有（音频）标记
        if '（音频）' not in content and '(音频)' not in content:
            violations.append(Violation(
                Severity.WARN,
                f"{spec.cn_name} 缺少（音频）标记",
                "对话类组件应在选项或题干后标注（音频）",
                "【互动内容】"
            ))
    return violations


def check_no_options_when_not_expected(text: str, spec: FormatSpec) -> List[Violation]:
    """非选项型组件不应出现选项"""
    violations = []
    if spec.has_options:
        return violations

    if re.search(r'选项\d*[：:]', text):
        violations.append(Violation(
            Severity.WARN,
            f"{spec.cn_name} 不应包含选项",
            f"该题型为{spec.notes}，但配置中出现了选项",
            "【互动内容】"
        ))
    return violations


# ============================================================
# 第三部分：主校验入口
# ============================================================

def validate(teaching_config: str, cn_type: str) -> List[Violation]:
    """
    校验单个组件配置

    Args:
        teaching_config: 组件配置文本（teaching_config 字段内容）
        cn_type: 组件中文类型名（如 "对话挖空"、"对话挖空12"）

    Returns:
        违规列表
    """
    spec, resolved_type = resolve_type(cn_type)
    violations = []

    if spec is None:
        violations.append(Violation(
            Severity.ERROR,
            "未知组件类型",
            f"'{cn_type}' 不在已知的 27 种中互动或 15 种核心互动类型中",
            ""
        ))
        return violations

    # ---- 通用规则 ----
    violations.extend(check_title_spacing(teaching_config))
    violations.extend(check_feedback_colon(teaching_config))
    violations.extend(check_markdown(teaching_config))
    violations.extend(check_spelling(teaching_config))
    violations.extend(check_punctuation(teaching_config))

    # ---- 题型专属规则 ----
    violations.extend(check_blank_marker(teaching_config, spec))
    violations.extend(check_question_language(teaching_config, spec))
    violations.extend(check_options_format(teaching_config, spec))
    violations.extend(check_audio_marker(teaching_config, spec))
    violations.extend(check_no_options_when_not_expected(teaching_config, spec))

    return violations


def format_report(violations: List[Violation], cn_type: str = "") -> str:
    """格式化输出校验报告"""
    if not violations:
        return "✅ 全部通过"

    errors = [v for v in violations if v.severity == Severity.ERROR]
    warns = [v for v in violations if v.severity == Severity.WARN]
    infos = [v for v in violations if v.severity == Severity.INFO]

    lines = []
    if cn_type:
        lines.append(f"📋 校验结果: {cn_type}")
    lines.append(f"   🔴 错误: {len(errors)}  🟡 警告: {len(warns)}  ⚪ 提示: {len(infos)}")
    lines.append("")

    for v in violations:
        lines.append(f"  {v.severity.value} [{v.rule}] {v.detail}")
        if v.location:
            lines.append(f"     位置: {v.location}")

    return '\n'.join(lines)


# ============================================================
# 第四部分：CLI 入口
# ============================================================

def main():
    parser = argparse.ArgumentParser(
        description="互动组件配置格式校验脚本",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 校验单个组件
  python3 validate_format.py "【任务标题】汇报Jay不配合\\n..." --type "对话挖空"

  # 从 stdin 读取
  echo "..." | python3 validate_format.py --type "对话组句" --stdin

  # 列出所有支持的题型
  python3 validate_format.py --list-types
        """
    )
    parser.add_argument('config_text', nargs='?', help='组件配置文本')
    parser.add_argument('--type', '-t', required=False, help='组件中文类型名')
    parser.add_argument('--stdin', action='store_true', help='从 stdin 读取配置文本')
    parser.add_argument('--list-types', action='store_true', help='列出所有支持的题型')
    parser.add_argument('--json', action='store_true', help='以 JSON 格式输出')

    args = parser.parse_args()

    if args.list_types:
        print("=== 中互动组件（27种）===")
        for name, spec in MID_FORMAT_SPECS.items():
            flags = []
            if spec.has_blank_marker: flags.append("___标记")
            if spec.has_options: flags.append("选项")
            if spec.has_answer: flags.append("答案")
            if spec.has_per_option_feedback: flags.append("逐项反馈")
            if spec.is_read_type: flags.append("朗读")
            if spec.is_express_type: flags.append("表达")
            if spec.is_sentence_build: flags.append("组句")
            print(f"  {name:12s} | {', '.join(flags) if flags else '(无特殊标记)'}")
        print("\n=== 核心互动组件（15种）===")
        for name in CORE_FORMAT_SPECS:
            print(f"  {name}")
        return

    if not args.type:
        print("❌ 必须指定 --type 参数", file=sys.stderr)
        sys.exit(1)

    if args.stdin:
        config_text = sys.stdin.read()
    elif args.config_text:
        config_text = args.config_text
    else:
        print("❌ 必须提供配置文本（参数或 --stdin）", file=sys.stderr)
        sys.exit(1)

    violations = validate(config_text, args.type)

    if args.json:
        result = {
            "type": args.type,
            "passed": len(violations) == 0,
            "error_count": len([v for v in violations if v.severity == Severity.ERROR]),
            "warn_count": len([v for v in violations if v.severity == Severity.WARN]),
            "violations": [
                {"severity": v.severity.value, "rule": v.rule, "detail": v.detail, "location": v.location}
                for v in violations
            ]
        }
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(format_report(violations, args.type))

    # 有错误时返回非零
    if any(v.severity == Severity.ERROR for v in violations):
        sys.exit(1)


if __name__ == '__main__':
    main()
