#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
提示词模板集中管理

所有对外 LLM 调用的 system_prompt 和 user_prompt 均在此定义。

================================================================================
使用场景总览
================================================================================

1. 主分类调用 (CLASSIFY_SYSTEM_PROMPT + build_classify_prompt)
   - 调用位置: ContentClassifierClient.classify_content() → _classify_single_chunk() → _call_api()
   - 触发时机: 对二级分类下的文档内容进行三级分类识别时
   - 输入: SectionContent（包含二级分类标准、带行号的文档内容）
   - 输出: JSON 格式的分类结果列表（category_index 为数字索引）
   - 特点:
     * 支持长内容分块处理（超过150行自动切分）
     * 使用数字索引避免模型输出复杂代码字符串
     * 包含详细的分类指南和示例

2. JSON 修复调用 (build_fix_prompt)
   - 调用位置: ContentClassifierClient._parse_with_fix()
   - 触发时机: 主分类返回的 JSON 解析失败时（最多重试3次）
   - 输入: 原始模型输出（格式错误的 JSON 字符串）
   - 输出: 修复后的合法 JSON
   - 特点: 严格保持业务数据完整性，仅修复语法错误

3. 补充验证调用 (SUPPLEMENT_VERIFY_SYSTEM_PROMPT + build_supplement_verify_prompt)
   - 调用位置: ContentClassifierClient._detect_and_supplement() → _call_supplement_verification()
   - 触发时机:
     * 主分类完成后，扫描发现某些三级分类可能遗漏时
     * 通过 keywords 字段匹配到未覆盖行时
     * 表格内容需要二次确认时
   - 输入: 待确认的 CategoryStandard + 相关文本片段
   - 输出: "存在" 或 "不存在"
   - 特点: 轻量级验证，max_tokens=10，temperature=0.0

================================================================================
调用流程图
================================================================================

classify_content()
    ├── Embedding 相似度检查（可选跳过）
    ├── _classify_single_chunk()
    │       ├── build_classify_prompt() ──→ LLM 主分类
    │       └── _parse_with_fix()
    │               └── build_fix_prompt() ──→ LLM 修复 JSON（失败时）
    ├── _detect_and_supplement()
    │       └── build_supplement_verify_prompt() ──→ LLM 确认遗漏（逐个标准）
    └── 合并结果并返回
"""

from typing import List

from .models import CategoryStandard, SectionContent


# ================================================================================
# 主分类调用
# ================================================================================

# 使用场景：ContentClassifierClient._call_api() 中的 system 消息
# 作用：定义模型角色为"施工方案文档分析专家"，约束输出格式和行为
CLASSIFY_SYSTEM_PROMPT = """你是专业的施工方案文档分析专家。你的任务是：
1. 仔细阅读文档内容，理解每行的语义
2. 将内容归类到给定的三级分类标准中
3. 【重要】优先使用标准分类，只有完全不符合时才使用索引0（非标准项）
4. 【重要】连续相同分类的多行必须合并为一个条目
5. 【重要】当一行同时提及多个主体或类别（如"勘察、设计和监测单位"），必须为每个主体单独输出一条条目，行号相同
6. 【重要】输出格式：category_index必须是纯数字（0,1,2...），禁止输出文本名称或代码
7. 必须在给定的三级分类标准范围内分类，禁止创造新的分类
8. 只输出JSON格式结果，不要任何解释文字"""


def build_classify_prompt(section: SectionContent, is_chunk: bool = False) -> str:
    """
    构建主分类的 user prompt。

    使用场景：
        ContentClassifierClient._classify_single_chunk() 中调用，
        将 SectionContent 转换为 LLM 可理解的分类任务描述。

    参数说明：
        section: 包含二级分类标准、文档内容和行号映射的段落对象
        is_chunk: 是否为分块处理（长文档会被切分成多个 chunk 依次处理）

    输出格式：
        完整的 user prompt 字符串，包含：
        - 当前文档位置（一级/二级分类信息）
        - 三级分类标准列表（带数字索引）
        - 带行号的文档内容
        - 分类任务指南（核心原则、示例、行号规则、多主体拆分规则）
        - 索引映射表（供后处理转换使用）
        - 强制约束（category_index 必须是数字）

    注意事项：
        - 内容超过 12000 字符会自动截断，并添加截断提示
        - 分块处理时会添加 chunk_hint 提示模型当前是文档的一部分
        - 使用数字索引（1-N）而非分类代码，避免模型输出错误
    """
    # 获取二级分类信息
    second_code = ""
    second_name = section.section_name
    first_code = ""
    first_name = ""

    if section.category_standards:
        first_code = section.category_standards[0].first_code
        first_name = section.category_standards[0].first_name
        second_code = section.category_standards[0].second_code

    # 构建三级分类标准描述（使用数字索引，模型只需输出索引号）
    standards_desc = []
    for i, std in enumerate(section.category_standards, 1):
        focus_content = std.third_focus if std.third_focus else "（无具体关注要点）"
        standards_desc.append(
            f"{i}. {std.third_name}\n"
            f"   【识别要点】{focus_content}"
        )

    # 添加非标准项作为兜底分类（索引0）
    standards_desc.insert(0, "0. 非标准项\n   【识别要点】仅当内容完全不符合以上任何分类标准时使用，如页眉页脚、纯表格分隔线、无关的广告语等")

    standards_text = '\n\n'.join(standards_desc) if standards_desc else "无具体标准，请根据内容自行判断"

    # 构建索引映射表（用于后处理转换）
    index_mapping_lines = ["0 -> 非标准项 (no_standard)"]
    for i, std in enumerate(section.category_standards, 1):
        index_mapping_lines.append(f"{i} -> {std.third_name} ({std.third_code})")
    index_mapping_text = "\n".join(index_mapping_lines)

    # 计算内容长度和分段提示
    max_content_length = 12000
    content_to_use = section.numbered_content[:max_content_length]
    is_truncated = len(section.numbered_content) > max_content_length

    if is_chunk and section.line_number_map:
        chunk_hint = (
            f"\n【注意】这是文档的一个分块（行号 {section.line_number_map[0]}~{section.line_number_map[-1]}），"
            f"请对有实质内容的行进行分类，空行和纯符号行无需单独输出。\n"
        )
    elif is_chunk:
        chunk_hint = "\n【注意】这是文档的一个分块，请对有实质内容的行进行分类。\n"
    else:
        chunk_hint = ""

    truncation_hint = (
        f"\n【提示】内容较长已截断，当前显示前{max_content_length}字符，请对显示的内容进行完整分类。\n"
        if is_truncated else ""
    )

    line_start = section.line_number_map[0] if section.line_number_map else 1
    line_end = section.line_number_map[-1] if section.line_number_map else len(section.lines)

    return f"""你是一个专业的施工方案文档分析专家。请根据给定的三级分类标准，识别文档内容中属于各个三级分类的部分。{chunk_hint}{truncation_hint}

## 当前文档位置
- 一级分类: {first_name} ({first_code})
- 二级分类: {second_name} ({second_code})

## 三级分类标准（共{len(section.category_standards)}个，必须在此范围内分类）

{standards_text}

---

## 文档内容（每行以<行号>开头，共{len(section.lines)}行）
```
{content_to_use}
```

---

## 分类任务指南

### 核心原则（按优先级排序）
1. **优先匹配标准分类**：首先判断内容是否符合上述任何一个三级分类标准
2. **关键词匹配**：内容中出现与分类名称相关的关键词时，应归类到该分类
3. **语义相关**：即使没有精确关键词，只要语义相关，也应归类
4. **非标准项谨慎使用**：只有当内容完全不符合任何标准分类时，才使用"非标准项"

### 分类示例
- 看到"验收内容"、"验收标准"、"验收程序"等内容 → 归类到对应的三级分类
- 看到"检验方法"、"检查内容"等 → 可能属于"检查要求"或"验收内容"
- 看到"材料"、"钢筋"、"混凝土"等 → 关注上下文判断所属三级分类

### 行号处理规则
- **必须合并连续行**：连续多行属于同一分类时，合并为一个条目（start_line为起始，end_line为结束）
- **禁止逐行输出**：不要为每一行单独创建条目
- **允许重复分类**：同一行内容可以同时属于多个三级分类

### 多主体句拆分规则（重要）
- 当一行内容同时提及多个不同主体或类别时，**必须为每个主体单独输出一条分类条目，行号相同**
- 示例：`"3、有关勘察、设计和监测单位项目技术负责人"` 同时涉及设计单位和监测单位，应输出：
  - `{{"third_category_code": "DesignUnitXxx", "start_line": N, "end_line": N}}`
  - `{{"third_category_code": "MonitoringUnitXxx", "start_line": N, "end_line": N}}`
- 示例：`"总承包单位和分包单位技术负责人"` 同时涉及施工单位，应归入施工单位对应分类
- 凡是"A、B和C单位"句式，需逐一判断每个主体能否对应某个三级分类

### 自查清单
- [ ] 是否优先使用了标准分类而非"非标准项"？
- [ ] 连续相同分类的行是否已合并？
- [ ] 分类名称是否与标准列表完全一致？
- [ ] 包含多个主体的行是否已拆分为多条输出？

## 索引映射表（用于后处理转换，你只需输出索引号）
{index_mapping_text}

## 输出格式（严格JSON，不要任何其他文字）
```{{
    "classified_contents_list": [
        {{
            "category_index": 数字索引号,
            "start_line": 起始行号,
            "end_line": 结束行号
        }}
    ]
}}
```

## 强制约束
1. **category_index 必须是数字**（0, 1, 2, 3...），对应上述索引映射表
2. 0 表示非标准项，1-{len(section.category_standards)} 对应各个三级分类
3. **禁止输出文本名称或代码**，只输出数字索引
4. 行号范围: {line_start} - {line_end}
5. 只输出JSON，禁止任何解释文字"""


# ================================================================================
# JSON 修复调用
# ================================================================================

def build_fix_prompt(original_response: str) -> str:
    """
    构建 JSON 格式修复的 user prompt。

    使用场景：
        ContentClassifierClient._parse_with_fix() 中调用，
        当主分类返回的 JSON 解析失败时，请求模型修复格式错误。

    参数说明：
        original_response: 原始模型输出，包含 JSON 格式错误（如缺少逗号、
                          括号不匹配、引号问题等）

    修复策略：
        1. 严格保持原始数据的完整性和内容，不修改业务数据
        2. 仅修复 JSON 语法错误（逗号、括号、引号等）
        3. 确保输出合法的 JSON 格式
        4. 强制 category_index 为数字索引

    注意事项：
        - 最多截取前 6000 字符进行修复（避免超出上下文限制）
        - 如果原始内容被截断，修复已提供的部分即可
        - 这是自动重试机制的一部分，最多重试 3 次
    """
    return f"""你之前的输出存在JSON格式错误，请修复以下内容为正确的JSON格式。

## 修复要求
1. 严格保持原始数据的完整性和内容，不要修改任何业务数据
2. 只修复JSON语法错误（如缺少逗号、括号不匹配、引号问题等）
3. 确保输出的是合法的JSON格式
4. 【重要】category_index 必须是数字索引（0, 1, 2...），禁止输出文本名称或代码
5. 输出必须严格符合以下结构：
{{
    "classified_contents_list": [
        {{
            "category_index": 数字索引号,
            "start_line": 数字,
            "end_line": 数字
        }}
    ]
}}

## 原始输出（需要修复的内容）
```
{original_response[:6000]}
```

注意：
- 只输出JSON，不要任何解释文字
- 如果原始内容被截断，修复已提供的部分即可
- category_index 只能是数字，如 0（非标准项）、1、2、3..."""


# ================================================================================
# 补充验证调用
# ================================================================================

# 使用场景：ContentClassifierClient._call_supplement_verification() 中的 system 消息
# 作用：定义模型角色为"内容审查专家"，约束输出为二值判断（存在/不存在）
SUPPLEMENT_VERIFY_SYSTEM_PROMPT = '你是施工方案内容审查专家，请根据提供的内容作出判断，只回答"存在"或"不存在"，不要任何其他文字。'


def build_supplement_verify_prompt(
    std: CategoryStandard,
    chunk_text: str,
    start: int,
    end: int,
    hit_lines: List[int],
    matched_kws: List[str],
    is_table: bool = False
) -> str:
    """
    构建补充验证的 user prompt。

    使用场景：
        ContentClassifierClient._detect_and_supplement() → _call_supplement_verification()
        在主分类完成后，对某些可能遗漏的三级分类进行二次确认。

    触发条件：
        1. 普通路径：扫描 section 全文，发现某些三级分类的 keywords 出现在
           未被 LLM 分类覆盖的行中
        2. 表格路径：section 包含表格特征（含"序号/作业活动/风险源/防范措施"
           或 | 符号较多），某些分类可能隐藏在表格列中

    参数说明：
        std: 待确认的三级分类标准（包含 third_name, third_focus, keywords）
        chunk_text: 待审查的文本片段（根据 hit_lines 提取）
        start: 文本片段起始行号
        end: 文本片段结束行号
        hit_lines: 匹配到 keyword 的行号列表
        matched_kws: 匹配到的关键词列表
        is_table: 是否为表格路径（影响 trigger 说明文字）

    输出格式：
        模型应只回答"存在"或"不存在"

    调用特点：
        - 轻量级调用：max_tokens=10, temperature=0.0
        - 逐个标准独立调用（非批量）
        - 包含组织层级说明（区分总公司/子公司/桥梁公司）
    """
    if is_table:
        trigger = "该内容块包含表格，表格中多列信息混排，以下分类在主分类阶段未被识别，需确认是否存在于表格中"
    else:
        trigger = f"以下关键字在文档中被检测到：{'、'.join(matched_kws)}（出现于第 {hit_lines} 行）"

    return f"""你是一个施工方案内容分类专家。

【组织层级说明】
本项目的组织层级如下，判断时请严格区分：
- 四川路桥（总公司）= 四川公路桥梁建设集团有限公司，文件通常以"四川公路桥梁"开头或含"SCQJ"
- 路桥集团（子公司）= 四川路桥集团有限公司，文件中出现"四川路桥集团"即属于路桥集团（子公司），而非总公司
- 桥梁公司（子公司）= 四川路桥桥梁公司，文件中出现"四川路桥桥梁公司"或"桥梁公司"即属于桥梁公司（子公司）

【待审查内容】（第 {start}~{end} 行）
{chunk_text}

【待确认的分类】
分类名称：{std.third_name}
识别说明：{std.third_focus}

【触发原因】
{trigger}

【问题】
上述文档内容中，是否包含"{std.third_name}"相关的实质内容？

请仅回答"存在"或"不存在"："""
