Documentation
use std::path::Path;

use serde_json::{json, Map, Value as JsonValue};

use crate::init::settings::{SettingId, Settings};

pub const SECTION_START: &str = "<!-- iwe -->";
pub const SECTION_END: &str = "<!-- /iwe -->";

pub fn instructions(settings: &Settings) -> String {
    let library = match settings.get(SettingId::LibraryPath).as_text() {
        Some(path) if !path.is_empty() => format!("{}/", path),
        _ => "the repository root".to_string(),
    };

    let wiki = settings.get(SettingId::LinkFormat).as_text() == Some("wiki");
    let extension = settings
        .get(SettingId::RefsExtension)
        .as_text()
        .unwrap_or_default()
        .to_string();

    let link_example = if wiki {
        if settings.get(SettingId::WikiLinkPath).as_text() == Some("short") {
            "[[roadmap]]".to_string()
        } else {
            "[[projects/roadmap]]".to_string()
        }
    } else {
        format!("[Roadmap](projects/roadmap{})", extension)
    };
    let link_rule = if wiki {
        "Link between documents with wiki links"
    } else {
        "Link between documents with regular markdown links"
    };

    let key_naming = match settings.get(SettingId::KeyTemplate).as_text() {
        Some("{{title}}") => "Filenames mirror the document title, spaces included.",
        Some("{{id}}") => "Filenames are numeric identifiers.",
        _ => "Filenames are lowercase slugs with hyphens between words.",
    };

    format!(
        "{start}\n\
        ## Notes in this repository\n\
        \n\
        These notes form an IWE graph. Keep the following conventions when editing them.\n\
        \n\
        - Documents live under {library} and are addressed by key — the path without the extension.\n\
        - Every document starts with a single `#` header; that header is its title.\n\
        - {link_rule}, for example `{link_example}`.\n\
        - {key_naming}\n\
        - Run `iwe normalize` after bulk edits so formatting stays consistent.\n\
        \n\
        Useful commands:\n\
        \n\
        - `iwe find <query>` — full-text search across the graph\n\
        - `iwe retrieve <key>` — read a document and the documents it includes\n\
        - `iwe create <key> -c <document>` — create a document from complete markdown\n\
        - `iwe update <key>` — apply a structured edit to a document\n\
        - `iwe normalize` — rewrite every document in canonical form\n\
        {end}\n",
        start = SECTION_START,
        end = SECTION_END,
        library = library,
        link_rule = link_rule,
        link_example = link_example,
        key_naming = key_naming,
    )
}

pub fn mcp_snippet() -> String {
    serde_json::to_string_pretty(&json!({
        "mcpServers": {
            "iwe": {
                "command": "iwec",
                "args": []
            }
        }
    }))
    .expect("snippet serializes")
        + "\n"
}

pub fn write_instructions(root: &Path, settings: &Settings) -> Result<String, String> {
    let target = root.join("AGENTS.md");
    let section = instructions(settings);

    let existing = std::fs::read_to_string(&target).unwrap_or_default();
    if existing.contains(SECTION_START) {
        return Ok("AGENTS.md already carries an iwe section — left untouched".to_string());
    }

    let updated = if existing.trim().is_empty() {
        section
    } else {
        format!("{}\n\n{}", existing.trim_end(), section)
    };

    std::fs::write(&target, updated)
        .map_err(|error| format!("failed to write AGENTS.md: {}", error))?;

    Ok(if existing.is_empty() {
        "created AGENTS.md".to_string()
    } else {
        "appended an iwe section to AGENTS.md".to_string()
    })
}

pub fn register_mcp(root: &Path) -> Result<String, String> {
    let target = root.join(".mcp.json");
    let existing = std::fs::read_to_string(&target).unwrap_or_default();

    let mut document: JsonValue = if existing.trim().is_empty() {
        json!({})
    } else {
        serde_json::from_str(&existing)
            .map_err(|error| format!("failed to parse .mcp.json: {}", error))?
    };

    let root_object = document
        .as_object_mut()
        .ok_or_else(|| ".mcp.json is not a JSON object".to_string())?;

    let servers = root_object
        .entry("mcpServers")
        .or_insert_with(|| JsonValue::Object(Map::new()));
    let servers = servers
        .as_object_mut()
        .ok_or_else(|| "mcpServers in .mcp.json is not an object".to_string())?;

    if servers.contains_key("iwe") {
        return Ok(".mcp.json already registers iwe — left untouched".to_string());
    }

    servers.insert("iwe".to_string(), json!({ "command": "iwec", "args": [] }));

    let rendered = serde_json::to_string_pretty(&document)
        .map_err(|error| format!("failed to render .mcp.json: {}", error))?;
    std::fs::write(&target, rendered + "\n")
        .map_err(|error| format!("failed to write .mcp.json: {}", error))?;

    Ok(if existing.trim().is_empty() {
        "created .mcp.json with the iwec server".to_string()
    } else {
        "registered the iwec server in .mcp.json".to_string()
    })
}

#[cfg(test)]
mod tests {
    use std::fs::{read_to_string, write};

    use tempfile::TempDir;

    use super::{instructions, register_mcp, write_instructions};
    use crate::init::settings::{defaults, Confidence, SettingId, Settings, Value};

    fn wiki_settings() -> Settings {
        let mut settings = defaults();
        settings.set(
            SettingId::LinkFormat,
            Value::text("wiki"),
            Confidence::Detected,
            "3 wiki links vs 0 markdown links",
        );
        settings
    }

    #[test]
    fn appends_an_iwe_section_to_an_existing_agents_file() {
        let temp = TempDir::new().expect("Should create temp directory");
        write(
            temp.path().join("AGENTS.md"),
            "# House rules\n\nBe careful.\n",
        )
        .expect("Should write AGENTS.md");
        let settings = wiki_settings();

        let message = write_instructions(temp.path(), &settings).expect("Should write section");

        let agents = read_to_string(temp.path().join("AGENTS.md")).expect("Should read AGENTS.md");
        assert_eq!("appended an iwe section to AGENTS.md", message);
        assert_eq!(
            format!(
                "# House rules\n\nBe careful.\n\n{}",
                instructions(&settings)
            ),
            agents
        );
    }

    #[test]
    fn leaves_an_existing_iwe_section_untouched() {
        let temp = TempDir::new().expect("Should create temp directory");
        let settings = wiki_settings();
        let existing = format!("# House rules\n\n{}", instructions(&settings));
        write(temp.path().join("AGENTS.md"), &existing).expect("Should write AGENTS.md");

        let message = write_instructions(temp.path(), &settings).expect("Should report");

        let agents = read_to_string(temp.path().join("AGENTS.md")).expect("Should read AGENTS.md");
        assert_eq!(
            "AGENTS.md already carries an iwe section — left untouched",
            message
        );
        assert_eq!(existing, agents);
    }

    #[test]
    fn short_wiki_paths_use_a_bare_link_example() {
        let mut settings = wiki_settings();
        settings.set(
            SettingId::WikiLinkPath,
            Value::text("short"),
            Confidence::Detected,
            "0 wiki links carry a path, 3 are bare names",
        );

        let section = instructions(&settings);
        let bare: Vec<&str> = section
            .lines()
            .filter(|line| line.starts_with("- Link between"))
            .map(|line| line.trim_end())
            .collect();

        assert_eq!(
            vec!["- Link between documents with wiki links, for example `[[roadmap]]`."],
            bare
        );
    }

    #[test]
    fn registers_the_mcp_server_alongside_existing_servers() {
        let temp = TempDir::new().expect("Should create temp directory");
        write(
            temp.path().join(".mcp.json"),
            "{\n  \"mcpServers\": {\n    \"other\": { \"command\": \"other\" }\n  }\n}\n",
        )
        .expect("Should write .mcp.json");

        let message = register_mcp(temp.path()).expect("Should register");

        let text = read_to_string(temp.path().join(".mcp.json")).expect("Should read .mcp.json");
        let document: serde_json::Value =
            serde_json::from_str(&text).expect(".mcp.json is valid JSON");
        assert_eq!("registered the iwec server in .mcp.json", message);
        assert_eq!(
            serde_json::json!({
                "mcpServers": {
                    "iwe": { "command": "iwec", "args": [] },
                    "other": { "command": "other" }
                }
            }),
            document
        );
    }
}