//! Input Augmentation 层领域类型
//!
//! 解决「@项目 / /技能 → AI 上下文」两个增强通道的结构性问题:
//! - [`MentionRef`]:resolve 前,前端选中 mention 后透传的原始引用(携带 id)
//! - [`Augmentation`]:resolve 后,注入 LLM 上下文的投影数据(已 join / 已脱敏)
//! - [`SanitizedPath`]:path 脱敏 newtype,防止裸 String 误用未脱敏值
//! - [`MentionSpanDto`]:chip 元数据,前端按 span 精确替换 mention token
//! - [`ResolveError`]:Resolver 投影失败原因
//!
//! serde 统一 `tag = "kind"`,新增变体向后兼容(老前端遇未知 kind 忽略)。
//! 设计详见 plan: mighty-wibbling-papert.md 核心设计 1。

use serde::{Deserialize, Serialize};

use crate::types::{IdeaStatus, ProjectId, ProjectStatus, TaskId, TaskStatus};

// ============================================================
// 路径脱敏 newtype
// ============================================================

/// 脱敏后的文件系统路径(已根据 provider locality 过滤)。
///
/// 用 newtype 包裹,强制调用方走 [`crate::augmentation`] 模块的脱敏入口,
/// 防止裸 `String` 误用未脱敏的原始路径(含用户名/公司目录/客户名)直接注入 LLM。
///
/// 脱敏规则由 `src-tauri` 侧 `augmentation::sanitize` 模块实现:
/// - Local provider(localhost/127.0.0.1/0.0.0.0/::1/ollama):保留全路径
/// - Remote provider:仅保留 basename
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SanitizedPath(String);

impl SanitizedPath {
    /// 构造一个已脱敏路径。仅在 sanitize 入口调用,业务代码不应直接构造。
    pub fn new(sanitized: impl Into<String>) -> Self {
        Self(sanitized.into())
    }

    /// 以字符串切片访问路径内容。
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// 取回内部的 String 所有权。
    pub fn into_inner(self) -> String {
        self.0
    }
}

impl From<SanitizedPath> for String {
    fn from(p: SanitizedPath) -> Self {
        p.0
    }
}

impl std::fmt::Display for SanitizedPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

// ============================================================
// MentionRef — resolve 前(前端透传)
// ============================================================

/// 前端选中 mention 后透传的原始引用(resolve 前)。
///
/// 携带 id(以及展示用 name),供后端 Resolver 投影成 [`Augmentation`]。
/// Skill 变体不带 id —— skill 以 name 为唯一键(无全局 id)。
///
/// serde `tag = "kind"`:序列化形如 `{"kind":"project","id":"...","name":"..."}`。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MentionRef {
    /// @项目
    Project {
        #[serde(rename = "id")]
        id: ProjectId,
        #[serde(rename = "name")]
        name: String,
    },
    /// @任务
    Task {
        #[serde(rename = "id")]
        id: TaskId,
        #[serde(rename = "name")]
        name: String,
    },
    /// @灵感
    Idea {
        #[serde(rename = "id")]
        id: String,
        #[serde(rename = "name")]
        name: String,
    },
    /// /技能(name 唯一键)
    Skill {
        #[serde(rename = "name")]
        name: String,
    },
}

impl MentionRef {
    /// 返回 mention 的 kind 标签(与 serde tag 一致):`project`/`task`/`idea`/`skill`。
    pub fn kind_str(&self) -> &'static str {
        match self {
            MentionRef::Project { .. } => "project",
            MentionRef::Task { .. } => "task",
            MentionRef::Idea { .. } => "idea",
            MentionRef::Skill { .. } => "skill",
        }
    }

    /// 返回展示用 label(即 mention 选中时的可见文本)。
    pub fn label(&self) -> &str {
        match self {
            MentionRef::Project { name, .. }
            | MentionRef::Task { name, .. }
            | MentionRef::Idea { name, .. }
            | MentionRef::Skill { name } => name,
        }
    }

    /// 返回 chip 渲染/查找用的稳定标识:有 id 用 id,Skill 用 name。
    pub fn ref_id(&self) -> &str {
        match self {
            MentionRef::Project { id, .. }
            | MentionRef::Task { id, .. }
            | MentionRef::Idea { id, .. } => id,
            MentionRef::Skill { name } => name,
        }
    }
}

// ============================================================
// Augmentation — resolve 后(注入 LLM 用)
// ============================================================

/// Resolver 投影后的增强上下文(resolve 后,注入用)。
///
/// 与 [`MentionRef`] 的区别:
/// - 字段更丰富(join 后的 status / description / project_name 等)
/// - `Project` 带 `path: Option<SanitizedPath>`(已脱敏)
/// - `Skill` 带 `body`(已剥 frontmatter 的正文)
///
/// serde `tag = "kind"`,与 `MentionRef` 同构,前端可共用 chip 渲染逻辑。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Augmentation {
    /// @项目 注入体
    Project {
        #[serde(rename = "id")]
        id: ProjectId,
        #[serde(rename = "name")]
        name: String,
        #[serde(rename = "status")]
        status: ProjectStatus,
        #[serde(rename = "description")]
        description: String,
        /// 脱敏后的项目路径(远程 provider 仅 basename / 本地全路径);无目录绑定时为 None。
        #[serde(rename = "path", default, skip_serializing_if = "Option::is_none")]
        path: Option<SanitizedPath>,
        /// Phase 4:项目关联信息(前 N 条任务/灵感/知识)。
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        extra: Vec<String>,
    },
    /// @任务 注入体
    Task {
        #[serde(rename = "id")]
        id: TaskId,
        #[serde(rename = "title")]
        title: String,
        #[serde(rename = "status")]
        status: TaskStatus,
        #[serde(rename = "description")]
        description: String,
        /// 所属项目名(join 展示用);游离任务为 None。
        #[serde(rename = "project_name", default, skip_serializing_if = "Option::is_none")]
        project_name: Option<String>,
        /// Phase 4:任务关联信息(关联任务/所属项目上下文)。
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        extra: Vec<String>,
    },
    /// @灵感 注入体
    Idea {
        #[serde(rename = "id")]
        id: String,
        #[serde(rename = "title")]
        title: String,
        #[serde(rename = "status")]
        status: IdeaStatus,
        #[serde(rename = "description")]
        description: String,
        /// Phase 4:灵感关联信息(已立项时所属项目)。
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        extra: Vec<String>,
    },
    /// /技能 注入体
    Skill {
        #[serde(rename = "name")]
        name: String,
        /// 技能来源(plugin/command/user 等),用于审计/展示。
        #[serde(rename = "source")]
        source: String,
        /// 已剥 frontmatter 的技能正文。
        #[serde(rename = "body")]
        body: String,
    },
}

impl Augmentation {
    /// 返回 kind 标签(与 serde tag 一致)。
    pub fn kind_str(&self) -> &'static str {
        match self {
            Augmentation::Project { .. } => "project",
            Augmentation::Task { .. } => "task",
            Augmentation::Idea { .. } => "idea",
            Augmentation::Skill { .. } => "skill",
        }
    }

    /// 返回注入段落的小标题(供 build_augmentation_segment 拼接)。
    pub fn section_label(&self) -> &str {
        match self {
            Augmentation::Project { .. } => "项目",
            Augmentation::Task { .. } => "任务",
            Augmentation::Idea { .. } => "灵感",
            Augmentation::Skill { .. } => "技能",
        }
    }
}

// ============================================================
// MentionSpanDto — chip 元数据
// ============================================================

/// 用户消息内 mention 区间的元数据,驱动 chip 精确替换。
///
/// 前端按 `{start, length}` 在原文中切出区间并替换为 chip;若区间被用户编辑破坏
/// (长度/内容不再匹配),降级为纯文本展示,避免正则误匹配。
///
/// 注意:序列化字段名 `refId`(camelCase,对齐前端 TS 类型),Rust 侧用 `ref_id`。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MentionSpanDto {
    /// 区间起点(字节/字符偏移由前端约定,后端仅透传)。
    #[serde(rename = "start")]
    pub start: u32,
    /// 区间长度。
    #[serde(rename = "length")]
    pub length: u32,
    /// kind 标签:`project`/`task`/`idea`/`skill`。
    #[serde(rename = "kind")]
    pub kind: String,
    /// 引用稳定标识(Project/Task/Idea 为 id,Skill 为 name),前端用于反查。
    #[serde(rename = "refId")]
    pub ref_id: String,
    /// chip 展示文本(项目名/任务标题/技能名)。
    #[serde(rename = "label")]
    pub label: String,
}

impl MentionSpanDto {
    /// 构造一个 span。kind 取自 MentionRef::kind_str,ref_id 取自 MentionRef::ref_id。
    pub fn new(start: u32, length: u32, kind: impl Into<String>, ref_id: impl Into<String>, label: impl Into<String>) -> Self {
        Self {
            start,
            length,
            kind: kind.into(),
            ref_id: ref_id.into(),
            label: label.into(),
        }
    }

    /// 区间终点(start + length)。
    pub fn end(&self) -> u32 {
        self.start.saturating_add(self.length)
    }
}

// ============================================================
// ResolveError — Resolver 投影失败
// ============================================================

/// Resolver 解析 [`MentionRef`] → [`Augmentation`] 的失败原因。
///
/// `Skip` 表示该 mention 无法 resolve 但不应中断整批注入(如 skill 已删除),
/// 调用方应跳过该项继续处理其他 mention。
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ResolveError {
    /// 引用对象不存在(项目/任务/灵感/技能已删除)。
    #[error("未找到引用: kind={kind}, ref_id={ref_id}")]
    NotFound {
        /// kind 标签。
        kind: String,
        /// 引用标识。
        ref_id: String,
    },

    /// kind 与 Resolver 注册不匹配(如 Project id 传给了 TaskResolver)。
    #[error("kind 不匹配: expected={expected}, actual={actual}")]
    KindMismatch {
        /// 期望的 kind。
        expected: String,
        /// 实际的 kind。
        actual: String,
    },

    /// 跳过该 mention(如 skill 已被卸载),不视为错误,不中断整批注入。
    #[error("跳过引用: kind={kind}, ref_id={ref_id}, reason={reason}")]
    Skip {
        /// kind 标签。
        kind: String,
        /// 引用标识。
        ref_id: String,
        /// 跳过原因(审计/日志用)。
        reason: String,
    },
}

// ============================================================
// 单测:serde round-trip + 辅助方法
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{IdeaStatus, ProjectStatus, TaskStatus};

    // ---------- SanitizedPath ----------

    #[test]
    fn sanitized_path_transparent_serde() {
        // serde(transparent):JSON 直接是裸字符串,无 {inner} 包裹
        let p = SanitizedPath::new("E:/wk-lab/devflow");
        let json = serde_json::to_string(&p).expect("serialize");
        assert_eq!(json, "\"E:/wk-lab/devflow\"");
        let back: SanitizedPath = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, p);
    }

    #[test]
    fn sanitized_path_accessors() {
        let p = SanitizedPath::new("/tmp/x");
        assert_eq!(p.as_str(), "/tmp/x");
        assert_eq!(p.to_string(), "/tmp/x");
        assert_eq!(p.into_inner(), "/tmp/x");
    }

    #[test]
    fn sanitized_path_from_to_string() {
        let p = SanitizedPath::new("a/b");
        let s: String = p.into();
        assert_eq!(s, "a/b");
    }

    // ---------- MentionRef round-trip ----------

    #[test]
    fn mention_ref_project_round_trip() {
        let m = MentionRef::Project {
            id: "p-1".to_string(),
            name: "devflow".to_string(),
        };
        let json = serde_json::to_string(&m).expect("serialize");
        // tag=kind, snake_case, 字段名 id/name
        assert_eq!(
            json,
            "{\"kind\":\"project\",\"id\":\"p-1\",\"name\":\"devflow\"}"
        );
        let back: MentionRef = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, m);
    }

    #[test]
    fn mention_ref_skill_round_trip() {
        let m = MentionRef::Skill {
            name: "review".to_string(),
        };
        let json = serde_json::to_string(&m).expect("serialize");
        assert_eq!(json, "{\"kind\":\"skill\",\"name\":\"review\"}");
        let back: MentionRef = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, m);
    }

    #[test]
    fn mention_ref_all_variants_round_trip() {
        let cases = vec![
            serde_json::to_value(MentionRef::Project {
                id: "p".into(),
                name: "pn".into(),
            })
            .unwrap(),
            serde_json::to_value(MentionRef::Task {
                id: "t".into(),
                name: "tn".into(),
            })
            .unwrap(),
            serde_json::to_value(MentionRef::Idea {
                id: "i".into(),
                name: "in".into(),
            })
            .unwrap(),
            serde_json::to_value(MentionRef::Skill { name: "s".into() }).unwrap(),
        ];
        for v in cases {
            let back: MentionRef = serde_json::from_value(v.clone()).expect("deserialize");
            let re = serde_json::to_value(&back).expect("serialize");
            assert_eq!(re, v, "round-trip not stable");
        }
    }

    #[test]
    fn mention_ref_helpers() {
        let p = MentionRef::Project {
            id: "p1".into(),
            name: "Devflow".into(),
        };
        assert_eq!(p.kind_str(), "project");
        assert_eq!(p.label(), "Devflow");
        assert_eq!(p.ref_id(), "p1");

        let s = MentionRef::Skill { name: "review".into() };
        assert_eq!(s.kind_str(), "skill");
        assert_eq!(s.ref_id(), "review"); // skill ref_id = name
        assert_eq!(s.label(), "review");
    }

    // ---------- Augmentation round-trip ----------

    #[test]
    fn augmentation_project_with_path_round_trip() {
        let a = Augmentation::Project {
            id: "p-1".to_string(),
            name: "devflow".to_string(),
            status: ProjectStatus::InProgress,
            description: "AI-native dev tool".to_string(),
            path: Some(SanitizedPath::new("E:/wk-lab/devflow")),
            extra: vec![],
        };
        let json = serde_json::to_string(&a).expect("serialize");
        assert!(json.contains("\"kind\":\"project\""));
        assert!(json.contains("\"status\":\"in_progress\""));
        assert!(json.contains("\"path\":\"E:/wk-lab/devflow\""));
        let back: Augmentation = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, a);
    }

    #[test]
    fn augmentation_project_path_none_omitted() {
        let a = Augmentation::Project {
            id: "p-2".to_string(),
            name: "nopath".to_string(),
            status: ProjectStatus::Planning,
            description: String::new(),
            path: None,
            extra: vec![],
        };
        let json = serde_json::to_string(&a).expect("serialize");
        // skip_serializing_if 生效:path 字段不出现在 JSON 中
        // 检查 "path" key(带引号),避免误匹配 name 值里的 path 子串
        assert!(!json.contains("\"path\""), "path should be omitted, got: {}", json);
        let back: Augmentation = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, a);
    }

    #[test]
    fn augmentation_task_with_project_name_round_trip() {
        let a = Augmentation::Task {
            id: "t-1".to_string(),
            title: "Implement X".to_string(),
            status: TaskStatus::Todo,
            description: "do X".to_string(),
            project_name: Some("devflow".to_string()),
            extra: vec![],
        };
        let json = serde_json::to_string(&a).expect("serialize");
        assert!(json.contains("\"kind\":\"task\""));
        assert!(json.contains("\"status\":\"todo\""));
        assert!(json.contains("\"project_name\":\"devflow\""));
        let back: Augmentation = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, a);
    }

    #[test]
    fn augmentation_idea_round_trip() {
        let a = Augmentation::Idea {
            id: "i-1".to_string(),
            title: "Multi-round parallel".to_string(),
            status: IdeaStatus::Approved,
            description: "desc".to_string(),
            extra: vec![],
        };
        let json = serde_json::to_string(&a).expect("serialize");
        assert!(json.contains("\"kind\":\"idea\""));
        assert!(json.contains("\"status\":\"approved\""));
        // idea 无 project_name / path 字段
        assert!(!json.contains("project_name"));
        assert!(!json.contains("\"path\""));
        let back: Augmentation = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, a);
    }

    #[test]
    fn augmentation_skill_round_trip() {
        let a = Augmentation::Skill {
            name: "review".to_string(),
            source: "command".to_string(),
            body: "## Review\nDo X".to_string(),
        };
        let json = serde_json::to_string(&a).expect("serialize");
        assert_eq!(
            json,
            "{\"kind\":\"skill\",\"name\":\"review\",\"source\":\"command\",\"body\":\"## Review\\nDo X\"}"
        );
        let back: Augmentation = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, a);
    }

    #[test]
    fn augmentation_helpers() {
        let p = Augmentation::Project {
            id: "p".into(),
            name: "n".into(),
            status: ProjectStatus::Planning,
            description: String::new(),
            path: None,
            extra: vec![],
        };
        assert_eq!(p.kind_str(), "project");
        assert_eq!(p.section_label(), "项目");

        let s = Augmentation::Skill {
            name: "x".into(),
            source: "y".into(),
            body: String::new(),
        };
        assert_eq!(s.kind_str(), "skill");
        assert_eq!(s.section_label(), "技能");
    }

    // ---------- MentionSpanDto round-trip ----------

    #[test]
    fn mention_span_dto_round_trip() {
        let span = MentionSpanDto::new(10, 8, "project", "p-1", "devflow");
        let json = serde_json::to_string(&span).expect("serialize");
        // refId camelCase
        assert!(json.contains("\"refId\":\"p-1\""));
        assert!(json.contains("\"kind\":\"project\""));
        assert!(json.contains("\"start\":10"));
        assert!(json.contains("\"length\":8"));
        assert!(json.contains("\"label\":\"devflow\""));
        let back: MentionSpanDto = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, span);
        assert_eq!(span.end(), 18);
    }

    #[test]
    fn mention_span_dto_end_saturates() {
        let span = MentionSpanDto::new(u32::MAX, 5, "task", "t", "lbl");
        // 防溢出:saturating_add 在 u32::MAX + 5 时停在上界
        assert_eq!(span.end(), u32::MAX);
    }

    // ---------- ResolveError ----------

    #[test]
    fn resolve_error_display() {
        let e = ResolveError::NotFound {
            kind: "project".into(),
            ref_id: "p-x".into(),
        };
        assert!(format!("{}", e).contains("p-x"));
        assert!(format!("{}", e).contains("project"));

        let e2 = ResolveError::KindMismatch {
            expected: "project".into(),
            actual: "task".into(),
        };
        let s = format!("{}", e2);
        assert!(s.contains("project") && s.contains("task"));

        let e3 = ResolveError::Skip {
            kind: "skill".into(),
            ref_id: "removed".into(),
            reason: "skill uninstalled".into(),
        };
        assert!(format!("{}", e3).contains("skill uninstalled"));
    }

    #[test]
    fn resolve_error_eq() {
        let a = ResolveError::NotFound {
            kind: "task".into(),
            ref_id: "t".into(),
        };
        let b = ResolveError::NotFound {
            kind: "task".into(),
            ref_id: "t".into(),
        };
        assert_eq!(a, b);
    }
}
