use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::ToolCallInput;
use axum::http::StatusCode;
use serde_json::{json, Value};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MnoteCapabilityPack {
    pub id: &'static str,
    pub title: &'static str,
    pub description: &'static str,
    pub category: &'static str,
    pub agent_ids: &'static [&'static str],
    pub read_only: bool,
    pub requires_context_refs: &'static [&'static str],
    pub tool_names: &'static [&'static str],
    pub content: &'static str,
}

pub type MnoteSkill = MnoteCapabilityPack;

const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
    MnoteCapabilityPack {
        id: "mnote-current-page",
        title: "当前页读取",
        description: "仅在任务需要当前 MNote Markdown 页面内容时读取当前页。",
        category: "mnote",
        agent_ids: &["hermes", "reasonix"],
        read_only: true,
        requires_context_refs: &["current_page"],
        tool_names: &[
            "mnote.context.snapshot",
            "mnote.context.resolve_target",
            "mnote.context.read_current_page",
        ],
        content: include_str!("../../../../../skills/mnote-current-page/SKILL.md"),
    },
    MnoteCapabilityPack {
        id: "mnote-knowledge-rag",
        title: "资料库问答",
        description:
            "通过 LightRAG provider 检索多本书、论文、PDF 和附件资料库，并返回可回跳来源。",
        category: "knowledge",
        agent_ids: &["hermes", "reasonix"],
        read_only: true,
        requires_context_refs: &["folder"],
        tool_names: &[
            "mnote.context.snapshot",
            "mnote.context.resolve_target",
            "mnote.knowledge_rag.status",
            "mnote.knowledge_rag.query",
            "mnote.knowledge_rag.open_reference",
        ],
        content: include_str!("../../../../../skills/mnote-knowledge-rag/SKILL.md"),
    },
    MnoteCapabilityPack {
        id: "mnote-local-file",
        title: "本地文件编辑",
        description: "在 MNote 授权目录内读取和修改本地 Markdown 文件。",
        category: "file",
        agent_ids: &["hermes", "reasonix"],
        read_only: false,
        requires_context_refs: &["current_page", "file", "folder"],
        tool_names: &["mnote.context.snapshot", "mnote.context.resolve_target"],
        content: include_str!("../../../../../skills/mnote-local-file/SKILL.md"),
    },
    MnoteCapabilityPack {
        id: "mnote-onlyoffice-live",
        title: "ONLYOFFICE 实时编辑",
        description: "操作当前已打开的 ONLYOFFICE Word、Excel、PPT 编辑会话。",
        category: "office",
        agent_ids: &["hermes", "reasonix"],
        read_only: false,
        requires_context_refs: &["onlyoffice"],
        tool_names: &[
            "mnote.onlyoffice.session.current",
            "mnote.onlyoffice.capabilities",
            "mnote.onlyoffice.selection.get",
            "mnote.onlyoffice.document.insert_text",
            "mnote.onlyoffice.document.replace_selection",
            "mnote.onlyoffice.document.insert_html",
            "mnote.onlyoffice.document.export",
            "mnote.onlyoffice.document.search_replace",
            "mnote.onlyoffice.document.insert_table",
            "mnote.onlyoffice.document.get_comments",
            "mnote.onlyoffice.document.add_comment",
            "mnote.onlyoffice.sheet.get_sheets",
            "mnote.onlyoffice.sheet.add_sheet",
            "mnote.onlyoffice.sheet.rename_sheet",
            "mnote.onlyoffice.sheet.get_range",
            "mnote.onlyoffice.sheet.get_range_values",
            "mnote.onlyoffice.sheet.get_values",
            "mnote.onlyoffice.sheet.set_value",
            "mnote.onlyoffice.sheet.set_formula",
            "mnote.onlyoffice.sheet.batch_set_values",
            "mnote.onlyoffice.sheet.set_range_values",
            "mnote.onlyoffice.sheet.format_range",
            "mnote.onlyoffice.sheet.set_dimensions",
            "mnote.onlyoffice.sheet.sort_range",
            "mnote.onlyoffice.sheet.add_chart",
            "mnote.onlyoffice.presentation.get_slides",
            "mnote.onlyoffice.presentation.get_slide_texts",
            "mnote.onlyoffice.presentation.get_shapes",
            "mnote.onlyoffice.presentation.add_text_slide",
            "mnote.onlyoffice.presentation.replace_text",
            "mnote.onlyoffice.presentation.set_shape_text",
            "mnote.onlyoffice.presentation.delete_slide",
            "mnote.onlyoffice.presentation.add_table",
            "mnote.onlyoffice.presentation.clear_slide",
            "mnote.onlyoffice.presentation.add_shape",
        ],
        content: include_str!("../../../../../skills/mnote-onlyoffice-live/SKILL.md"),
    },
    MnoteCapabilityPack {
        id: "mnote-mindmap",
        title: "思维导图",
        description: "读取、更新、总结或创建 MNote 思维导图资源。",
        category: "resource",
        agent_ids: &["hermes", "reasonix"],
        read_only: false,
        requires_context_refs: &["current_page", "file", "folder", "resource"],
        tool_names: &[
            "mnote.context.snapshot",
            "mnote.context.resolve_target",
            "mnote.mindmap.fetch",
            "mnote.mindmap.apply_ops",
            "mnote.mindmap.create_from_outline",
        ],
        content: include_str!("../../../../../skills/mnote-mindmap/SKILL.md"),
    },
    MnoteCapabilityPack {
        id: "mnote-chat-only",
        title: "纯聊天",
        description: "只进行对话回复，不读取或写入 MNote 页面、文件上下文。",
        category: "chat",
        agent_ids: &["chat_only", "hermes", "reasonix"],
        read_only: true,
        requires_context_refs: &[],
        tool_names: &[],
        content: include_str!("../../../../../skills/mnote-chat-only/SKILL.md"),
    },
];

pub fn skill_summaries_for_agent(agent_id: Option<&str>) -> Vec<Value> {
    capability_summaries_for_agent(agent_id)
}

pub fn capability_summaries_for_agent(agent_id: Option<&str>) -> Vec<Value> {
    CAPABILITY_PACKS
        .iter()
        .filter(|skill| skill_matches_agent(skill, agent_id))
        .map(skill_summary)
        .collect()
}

pub fn public_capability_packs() -> &'static [MnoteCapabilityPack] {
    CAPABILITY_PACKS
}

pub fn find_skill(skill_id: &str, agent_id: Option<&str>) -> Option<&'static MnoteSkill> {
    find_capability_pack(skill_id, agent_id)
}

pub fn find_capability_pack(
    capability_id: &str,
    agent_id: Option<&str>,
) -> Option<&'static MnoteCapabilityPack> {
    let requested = canonical_skill_id(capability_id.trim());
    CAPABILITY_PACKS
        .iter()
        .find(|skill| skill.id == requested && skill_matches_agent(skill, agent_id))
}

pub fn capability_ids_for_tool(tool_name: &str) -> Vec<&'static str> {
    let name = tool_name.trim();
    if name.is_empty() {
        return Vec::new();
    }
    CAPABILITY_PACKS
        .iter()
        .filter(|pack| pack.tool_names.iter().any(|tool| *tool == name))
        .map(|pack| pack.id)
        .collect()
}

pub fn manifest_capabilities() -> Vec<Value> {
    CAPABILITY_PACKS
        .iter()
        .map(|pack| {
            json!({
                "id": pack.id,
                "title": pack.title,
                "description": pack.description,
                "category": pack.category,
                "agentIds": pack.agent_ids,
                "readOnly": pack.read_only,
                "requiresContextRefs": pack.requires_context_refs,
                "skillId": pack.id,
                "toolNames": pack.tool_names,
                "uiKind": if pack.category == "chat" { "chat" } else { "ai_capability" },
                "public": true
            })
        })
        .collect()
}

fn canonical_skill_id(skill_id: &str) -> &str {
    match skill_id {
        "mnote-document-evidence" | "mnote-local-index" => "mnote-knowledge-rag",
        other => other,
    }
}

pub async fn skill_read(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let skill_id = input
        .arg_string("skillId")
        .or_else(|| input.arg_string("skill_id"))
        .ok_or_else(|| {
            WebError::bad_request_code("mnote_skill_id_required", "缺少 skillId")
                .with_context(context)
        })?;
    let agent_id = input
        .arg_string("agentId")
        .or_else(|| input.arg_string("agent_id"));
    let skill = find_skill(&skill_id, agent_id.as_deref()).ok_or_else(|| {
        WebError::new(
            StatusCode::NOT_FOUND,
            "mnote_skill_not_found",
            "未知或当前 agent 不可用的 MNote skill",
        )
        .with_context(context)
    })?;
    Ok(json!({
        "ok": true,
        "schema": "mnote.skill.v1",
        "skill": skill_summary(skill),
        "content": skill.content,
        "tools": skill.tool_names,
        "constraints": {
            "allowedRootsRequired": skill.requires_context_refs.contains(&"folder"),
            "mustReadBackAfterWrite": !skill.read_only
        }
    }))
}

fn skill_matches_agent(skill: &MnoteSkill, agent_id: Option<&str>) -> bool {
    let Some(agent_id) = agent_id.map(str::trim).filter(|value| !value.is_empty()) else {
        return true;
    };
    skill
        .agent_ids
        .iter()
        .any(|candidate| *candidate == agent_id)
}

fn skill_summary(skill: &MnoteSkill) -> Value {
    json!({
        "id": skill.id,
        "title": skill.title,
        "description": skill.description,
        "category": skill.category,
        "agentIds": skill.agent_ids,
        "readOnly": skill.read_only,
        "requiresContextRefs": skill.requires_context_refs,
        "toolNames": skill.tool_names
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn skill_registry_filters_by_agent() {
        let chat_skills = skill_summaries_for_agent(Some("chat_only"));
        assert!(chat_skills
            .iter()
            .any(|skill| skill["id"] == "mnote-chat-only"));
        assert!(!chat_skills
            .iter()
            .any(|skill| skill["id"] == "mnote-local-file"));
    }

    #[test]
    fn skill_lookup_rejects_unknown_or_agent_mismatch() {
        assert!(find_skill("mnote-current-page", Some("reasonix")).is_some());
        assert!(find_skill("mnote-local-file", Some("chat_only")).is_none());
        assert!(find_skill("missing", Some("reasonix")).is_none());
    }

    #[test]
    fn skill_registry_exposes_onlyoffice_live_skill_to_agents() {
        let reasonix_skills = skill_summaries_for_agent(Some("reasonix"));
        let skill = reasonix_skills
            .iter()
            .find(|skill| skill["id"] == "mnote-onlyoffice-live")
            .expect("reasonix should see live ONLYOFFICE skill");
        assert_eq!(skill["readOnly"], false);
        assert_eq!(
            skill["toolNames"]
                .as_array()
                .expect("tool names")
                .iter()
                .any(|name| name == "mnote.onlyoffice.session.current"),
            true
        );
        assert_eq!(
            skill["toolNames"]
                .as_array()
                .expect("tool names")
                .iter()
                .any(|name| name == "mnote.onlyoffice.sheet.batch_set_values"),
            true
        );
        assert_eq!(
            skill["toolNames"]
                .as_array()
                .expect("tool names")
                .iter()
                .any(|name| name == "mnote.onlyoffice.sheet.add_chart"),
            true
        );
        assert_eq!(
            skill["toolNames"]
                .as_array()
                .expect("tool names")
                .iter()
                .any(|name| name == "mnote.onlyoffice.presentation.add_shape"),
            true
        );
        assert_eq!(
            skill["toolNames"]
                .as_array()
                .expect("tool names")
                .iter()
                .any(|name| name == "mnote.onlyoffice.presentation.replace_text"),
            true
        );
    }

    #[test]
    fn skill_registry_exposes_mindmap_skill_to_agents() {
        let reasonix_skills = skill_summaries_for_agent(Some("reasonix"));
        let skill = reasonix_skills
            .iter()
            .find(|skill| skill["id"] == "mnote-mindmap")
            .expect("reasonix should see mindmap skill");
        assert_eq!(skill["readOnly"], false);
        assert!(skill["requiresContextRefs"]
            .as_array()
            .expect("context refs")
            .iter()
            .any(|value| value == "resource"));
        assert!(skill["toolNames"]
            .as_array()
            .expect("tool names")
            .iter()
            .any(|name| name == "mnote.mindmap.create_from_outline"));
    }

    #[test]
    fn skill_registry_retired_local_index_in_favor_of_lightrag() {
        let hermes_skills = skill_summaries_for_agent(Some("hermes"));
        assert!(!hermes_skills
            .iter()
            .any(|skill| skill["id"] == "mnote-local-index"));
        let skill = hermes_skills
            .iter()
            .find(|skill| skill["id"] == "mnote-knowledge-rag")
            .expect("hermes should see LightRAG skill");
        assert_eq!(skill["readOnly"], true);
        assert!(skill["toolNames"]
            .as_array()
            .expect("tool names")
            .iter()
            .any(|name| name == "mnote.knowledge_rag.query"));
        assert!(!hermes_skills
            .iter()
            .any(|skill| skill["id"] == "mnote-document-evidence"));
    }

    #[test]
    fn skill_read_maps_document_evidence_alias_to_lightrag() {
        let skill = find_skill("mnote-document-evidence", Some("reasonix"))
            .expect("compat alias should resolve");
        assert_eq!(skill.id, "mnote-knowledge-rag");
    }

    #[tokio::test]
    async fn skill_read_returns_mindmap_skill_content() {
        let context = RequestContext::from_http_parts(
            &axum::http::Method::POST,
            &"/api/hermes/tools/execute".parse().expect("uri"),
            &axum::http::HeaderMap::new(),
        );
        let input: ToolCallInput = serde_json::from_value(json!({
            "toolName": "mnote.skill.read",
            "args": {
                "skillId": "mnote-mindmap",
                "agentId": "reasonix"
            }
        }))
        .expect("input");

        let payload = skill_read(&context, &input).await.expect("skill read");

        assert_eq!(payload["skill"]["id"], "mnote-mindmap");
        assert!(payload["content"]
            .as_str()
            .unwrap_or_default()
            .contains("mnote.mindmap.create_from_outline"));
        let content = payload["content"].as_str().unwrap_or_default();
        assert!(content.contains("Mindmap outline format"));
        assert!(content.contains("Use concise keywords or short phrases"));
        assert!(content.contains("Avoid long paragraphs"));
        assert!(payload["tools"]
            .as_array()
            .expect("tools")
            .iter()
            .any(|tool| tool == "mnote.mindmap.create_from_outline"));
    }
}
