//! 对抗评估 — 纯函数 / 类型 / 常量（自 adversarial.rs 抽离，纯搬迁，零行为变更）。
//!
//! 本文件承载 [`AdversarialEval`] 及其字段类型、LLM prompt 构造与 JSON 解析等无副作用
//! 逻辑；有状态的引擎 [`AdversarialEngine`]（含 evaluate / evaluate_with_llm 主入口与
//! ARC-260618-01-e 待决策逻辑）仍留在 `adversarial.rs`，二者经 [`use`] 互相引用。
//!
//! 抽离边界：类型定义（非 impl）+ 常量 + 顶层 free function。impl 主体不动，外部
//! 已用路径（`df_ideas::adversarial::{AdversarialEngine, AdversarialEval, Recommendation}` 等）
//! 由 adversarial.rs 通过 `pub use` 重新导出，调用方零变更。

use anyhow::Result;
use serde::{Deserialize, Serialize};

use df_types::types::{IdeaId, Priority};
use crate::capture::Idea;

/// 评估来源标记
///
/// `Default = Heuristic`：老数据（F-07 之前）序列化时无 evaluated_by 字段,
/// 反序列化回落启发式（与 F-07 之前行为一致）。
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum EvaluatedBy {
    /// LLM 深度评估
    Llm,
    /// 启发式评估（无 LLM 配置时的默认模式，也是老数据反序列化默认值）
    #[default]
    Heuristic,
    /// 启发式降级（LLM 调用失败后 fallback）
    HeuristicFallback,
}

/// 对抗评估结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdversarialEval {
    pub idea_id: IdeaId,
    pub positive: Argument,
    pub negative: Argument,
    pub analyst: AnalystAnalysis,
    pub final_score: f64,
    pub recommendation: Recommendation,
    /// 评估来源（Llm / Heuristic / HeuristicFallback），前端据此显示评估深度标签
    #[serde(default)]
    pub evaluated_by: EvaluatedBy,
}

/// 论点（正方/反方共用同一结构）
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Argument {
    pub thesis: String,           // 核心观点
    pub evidence: Vec<String>,    // 证据支持
    pub reasoning: Vec<String>,   // 推理过程
    pub confidence: f64,          // 置信度 0-1
}

/// AI 分析师综合分析
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalystAnalysis {
    pub summary: String,                    // 综合总结
    pub strengths: Vec<String>,             // 主要优势
    pub weaknesses: Vec<String>,            // 主要劣势
    pub risks: Vec<String>,                // 潜在风险
    pub opportunities: Vec<String>,          // 机会点
    pub final_assessment: AssessmentLevel,  // 最终评估
}

/// 评估等级
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AssessmentLevel {
    StrongGo,      // 强烈推荐执行
    Recommended,    // 推荐执行
    Conditional,   // 有条件执行
    Revised,       // 需要修改后执行
    Defer,         // 推迟执行
}

/// 最终建议
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Recommendation {
    ImmediateAction,   // 立即行动
    Soon,              // 尽快行动
    WithResources,      // 配置资源后行动
    ResearchMore,      // 需要更多研究
    Monitor,           // 持续监控
}

// ============================================================
// LLM 对抗评估 — prompt 构造 / JSON 解析
// ============================================================

/// LLM 角色 / 输出契约的系统级约束。
///
/// 放为 const 便于在 prompt 头部注入，与 [`build_adversarial_prompt`] 的「想法上下文」
/// 分离：角色设定稳定不变，想法上下文按评估对象动态拼。
pub(crate) const SYSTEM_PROMPT: &str = "\
你是一位资深的技术产品决策顾问。你将主持一场三角色对抗式辩论，对一个软件想法做结构化评估：
- 正方（Advocate）：论证为什么应该立即推进该想法，挖掘价值与可行性证据。
- 反方（Skeptic）：质疑该想法，挖掘风险、机会成本与替代方案，不放过任何隐患。
- 分析师（Analyst）：综合正反方观点，给出客观、平衡的最终裁决。

规则：
1. 你必须只输出一个 JSON 对象，不要输出任何解释性文字、Markdown 代码块标记或前后缀。
2. 所有枚举字段只能取下方 schema 列出的字符串字面量（区分大小写）。
3. 数值字段必须落在指定区间内。
4. 论点（thesis / evidence / reasoning / summary 等）用中文表达，简洁有信息密度。";

/// 构造对抗评估 prompt（三角色辩论 + 严格 JSON schema）。
///
/// 输出 prompt 包含：想法上下文（title/description/priority/tags）+ 角色 + JSON 输出
/// 契约（含字段说明与枚举字面量），让 LLM 单轮产出可解析的结构化评估。
pub(crate) fn build_adversarial_prompt(idea: &Idea) -> String {
    // 描述截断防 prompt 过长：> 800 字按 800 截并附省略号提示。
    let desc_raw = idea.description.trim();
    let description = if desc_raw.chars().count() > 800 {
        let head: String = desc_raw.chars().take(800).collect();
        format!("{head}……（描述已截断，原长超过 800 字）")
    } else if desc_raw.is_empty() {
        "（描述为空，需求边界待补充）".to_string()
    } else {
        desc_raw.to_string()
    };

    let tags = if idea.tags.is_empty() {
        "（无标签）".to_string()
    } else {
        idea.tags.join("、")
    };

    format!(
        "\
请对以下软件想法进行三角色对抗式评估。

【想法上下文】
标题：{title}
描述：{description}
优先级：{priority}
标签：{tags}

【输出 JSON schema】（只输出该 JSON 对象，字段名与枚举字面量严格一致）
{{
  \"positive\": {{
    \"thesis\": \"正方核心观点（一句话）\",
    \"evidence\": [\"证据 1\", \"证据 2\"],
    \"reasoning\": [\"推理 1\", \"推理 2\"],
    \"confidence\": 0.0到1.0之间的浮点数，正方置信度
  }},
  \"negative\": {{
    \"thesis\": \"反方核心观点（一句话）\",
    \"evidence\": [\"证据 1\"],
    \"reasoning\": [\"推理 1\"],
    \"confidence\": 0.0到1.0之间的浮点数，反方置信度
  }},
  \"analyst\": {{
    \"summary\": \"分析师综合总结（一段话）\",
    \"strengths\": [\"主要优势 1\"],
    \"weaknesses\": [\"主要劣势 1\"],
    \"risks\": [\"潜在风险 1\"],
    \"opportunities\": [\"机会点 1\"],
    \"final_assessment\": \"StrongGo | Recommended | Conditional | Revised | Defer\"
  }},
  \"final_score\": 0到10之间的浮点数，综合评分,
  \"recommendation\": \"ImmediateAction | Soon | WithResources | ResearchMore | Monitor\"
}}

枚举字段说明：
- final_assessment: StrongGo=强烈推荐执行 / Recommended=推荐执行 / Conditional=有条件执行 / Revised=需修改后执行 / Defer=推迟执行
- recommendation: ImmediateAction=立即行动 / Soon=尽快行动 / WithResources=配置资源后行动 / ResearchMore=需更多研究 / Monitor=持续监控

要求：
- positive 与 negative 的论点必须真实基于上方想法上下文，不要泛泛而谈。
- analyst.summary 必须总结双方并给出明确倾向，不要模棱两可。
- final_score 与 recommendation 应与 analyst.final_assessment 自洽（如 Defer 对应低分与 Monitor）。
- 只输出 JSON，不要任何额外文字。",
        title = idea.title,
        description = description,
        priority = priority_label(&idea.priority),
        tags = tags,
    )
}

/// LLM 返回的非类型化 JSON 表示（与 [`AdversarialEval`] 结构对齐，但枚举为字符串、
/// evaluated_by 字段省略——由 [`AdversarialEngine::evaluate`] 调用方覆盖）。
///
/// 采用独立中间结构而非直接 serde 到 [`AdversarialEval`]：便于在枚举非法时给出
/// 字段级错误信息（`final_assessment: "GoNow" 不在合法集合`），而非整条记录丢弃；
/// 同时留出数值 clamp 的统一收口。
#[derive(Debug, Deserialize)]
pub(crate) struct LlmEvalRaw {
    pub(crate) positive: ArgumentRaw,
    pub(crate) negative: ArgumentRaw,
    pub(crate) analyst: AnalystRaw,
    pub(crate) final_score: f64,
    pub(crate) recommendation: String,
}

#[derive(Debug, Deserialize)]
pub(crate) struct ArgumentRaw {
    pub(crate) thesis: String,
    #[serde(default)]
    pub(crate) evidence: Vec<String>,
    #[serde(default)]
    pub(crate) reasoning: Vec<String>,
    #[serde(default)]
    pub(crate) confidence: f64,
}

#[derive(Debug, Deserialize)]
pub(crate) struct AnalystRaw {
    #[serde(default)]
    pub(crate) summary: String,
    #[serde(default)]
    pub(crate) strengths: Vec<String>,
    #[serde(default)]
    pub(crate) weaknesses: Vec<String>,
    #[serde(default)]
    pub(crate) risks: Vec<String>,
    #[serde(default)]
    pub(crate) opportunities: Vec<String>,
    pub(crate) final_assessment: String,
}

/// 解析 LLM 返回文本为 [`AdversarialEval`]。
///
/// 容错策略（任一失败 `bail` → 由 [`AdversarialEngine::evaluate`] 降级启发式）：
/// 1. 剥离 ```json … ``` 代码块围栏与首尾空白（部分 LLM 会无视「只输出 JSON」要求）。
/// 2. `serde_json` 反序列化为 [`LlmEvalRaw`]；字段类型错 / 缺失必填 → bail。
/// 3. 枚举字符串映射（[`parse_assessment_level`] / [`parse_recommendation`]）；非法值 → bail。
/// 4. 数值 clamp：confidence ∈ [0,1]，final_score ∈ [0,10]。
pub(crate) fn parse_llm_eval(text: &str, idea_id: &str) -> Result<AdversarialEval> {
    let json_str = extract_json(text);
    let raw: LlmEvalRaw = serde_json::from_str(&json_str).map_err(|e| {
        anyhow::anyhow!("LLM 返回非合法 JSON 或字段缺失: {e}")
    })?;

    let final_assessment = parse_assessment_level(&raw.analyst.final_assessment)?;
    let recommendation = parse_recommendation(&raw.recommendation)?;

    let positive = Argument {
        thesis: raw.positive.thesis,
        evidence: raw.positive.evidence,
        reasoning: raw.positive.reasoning,
        confidence: raw.positive.confidence.clamp(0.0, 1.0),
    };
    let negative = Argument {
        thesis: raw.negative.thesis,
        evidence: raw.negative.evidence,
        reasoning: raw.negative.reasoning,
        confidence: raw.negative.confidence.clamp(0.0, 1.0),
    };
    let analyst = AnalystAnalysis {
        summary: raw.analyst.summary,
        strengths: raw.analyst.strengths,
        weaknesses: raw.analyst.weaknesses,
        risks: raw.analyst.risks,
        opportunities: raw.analyst.opportunities,
        final_assessment,
    };

    Ok(AdversarialEval {
        idea_id: idea_id.to_string(),
        positive,
        negative,
        analyst,
        // final_score clamp 单点收口于此（evaluate_with_llm 不再重复 clamp，CR-40-1）
        final_score: raw.final_score.clamp(0.0, 10.0),
        recommendation,
        // 由 evaluate() 按 LLM 调度路径覆盖为 EvaluatedBy::Llm
        evaluated_by: EvaluatedBy::Llm,
    })
}

/// 从 LLM 返回文本中提取 JSON 主体。
///
/// 提取优先级：
/// 1. 代码围栏在开头（```json … ``` 或 ``` … ```）→ 快路径剥离围栏返回主体。
/// 2. 围栏不在开头（LLM 输出「前缀文字 + ```json + {...} + ```」）→ 正则
///    `(?s)\{.*\}` 兜底提取首个 `{` 到最后一个 `}` 的片段（增强命中率）。
///
/// 注意：兜底返回的是「原始文本中首个 JSON 对象字面量」，**不再** trim 后整段交给
/// serde——由 [`parse_llm_eval`] 的 serde 步骤校验合法性，失败即 bail 降级。
pub(crate) fn extract_json(text: &str) -> String {
    let trimmed = text.trim();
    // 快路径：围栏在开头（```json ... ``` 或 ``` ... ```）
    if let Some(rest) = trimmed.strip_prefix("```") {
        // 跳过语言标记（json/JSON 等）到首个换行
        let after_lang = match rest.find('\n') {
            Some(idx) => &rest[idx + 1..],
            None => rest,
        };
        let body = after_lang.trim_end();
        if let Some(body_inner) = body.strip_suffix("```") {
            return body_inner.trim().to_string();
        }
        // 围栏闭合缺失(如 JSON 与 ``` 同行 / 闭合后仍有文字 / 多行 JSON 末尾混噪声):
        // 快路径无法干净剥离 → 落到正则兜底提取首个 JSON 对象,提升命中率。
        // (原实现在此处 return body.trim() 会把语言标记/尾随文字一起喂 serde,
        //  必然失败降级启发式,即便正则本可救回。)
    }
    // 兜底：围栏不在开头或混杂前后文字 → 正则提取首个 JSON 对象（CR-40-2）。
    // (?s) 让 . 匹配换行，贪婪 {*} 取首 { 到末 }，覆盖嵌套对象。
    // 提取失败（无 { }）则返回 trimmed 走原文 serde 报错降级，语义不变。
    static JSON_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
    let re = JSON_RE.get_or_init(|| regex::Regex::new(r"(?s)\{.*\}").expect("合法静态正则"));
    match re.captures(trimmed) {
        Some(c) => c.get(0).map(|m| m.as_str().to_string()).unwrap_or_else(|| trimmed.to_string()),
        None => trimmed.to_string(),
    }
}

/// 枚举字面量 → [`AssessmentLevel`]。区分大小写匹配 schema 文档约定。
pub(crate) fn parse_assessment_level(s: &str) -> Result<AssessmentLevel> {
    match s.trim() {
        "StrongGo" => Ok(AssessmentLevel::StrongGo),
        "Recommended" => Ok(AssessmentLevel::Recommended),
        "Conditional" => Ok(AssessmentLevel::Conditional),
        "Revised" => Ok(AssessmentLevel::Revised),
        "Defer" => Ok(AssessmentLevel::Defer),
        other => anyhow::bail!(
            "final_assessment 枚举值非法: {:?}（合法: StrongGo|Recommended|Conditional|Revised|Defer）",
            other
        ),
    }
}

/// 枚举字面量 → [`Recommendation`]。区分大小写匹配 schema 文档约定。
pub(crate) fn parse_recommendation(s: &str) -> Result<Recommendation> {
    match s.trim() {
        "ImmediateAction" => Ok(Recommendation::ImmediateAction),
        "Soon" => Ok(Recommendation::Soon),
        "WithResources" => Ok(Recommendation::WithResources),
        "ResearchMore" => Ok(Recommendation::ResearchMore),
        "Monitor" => Ok(Recommendation::Monitor),
        other => anyhow::bail!(
            "recommendation 枚举值非法: {:?}（合法: ImmediateAction|Soon|WithResources|ResearchMore|Monitor）",
            other
        ),
    }
}

pub(crate) fn priority_label(p: &Priority) -> &'static str {
    match p {
        Priority::Critical => "紧急",
        Priority::High => "高",
        Priority::Medium => "中",
        Priority::Low => "低",
    }
}

pub(crate) fn assessment_desc(level: &AssessmentLevel) -> &'static str {
    match level {
        AssessmentLevel::StrongGo => "价值高且可行性强",
        AssessmentLevel::Recommended => "整体值得推进",
        AssessmentLevel::Conditional => "有条件地推进",
        AssessmentLevel::Revised => "需调整后再评估",
        AssessmentLevel::Defer => "建议暂缓",
    }
}

pub(crate) fn action_hint(level: &AssessmentLevel) -> &'static str {
    match level {
        AssessmentLevel::StrongGo => "立即立项启动",
        AssessmentLevel::Recommended => "尽快排期",
        AssessmentLevel::Conditional => "配置资源后启动",
        AssessmentLevel::Revised => "补充信息后重新评估",
        AssessmentLevel::Defer => "持续观察时机",
    }
}
