use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use serde_json::json;
use std::path::PathBuf;

/// Scribe: context-aware file editor — create, replace, append, or delete file content via MCP.
pub struct ScribeSkill;

impl Skill for ScribeSkill {
    fn id(&self) -> SkillId {
        SkillId("scribe")
    }
    fn display_name(&self) -> &str {
        "Scribe"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "File path to operate on (relative to cwd or absolute)"
                },
                "operation": {
                    "type": "string",
                    "enum": ["create", "replace_str", "replace_all", "append", "delete", "insert_at"],
                    "description": "create: write new file (fails if exists without overwrite:true). replace_str: replace first occurrence of old_string with new_string. replace_all: replace every occurrence. append: add content to end of file. delete: remove the file. insert_at: insert content before a given line number."
                },
                "content": {
                    "type": "string",
                    "description": "Content to write (required for create, append, insert_at)"
                },
                "old_string": {
                    "type": "string",
                    "description": "String to find (required for replace_str, replace_all)"
                },
                "new_string": {
                    "type": "string",
                    "description": "Replacement string (required for replace_str, replace_all)"
                },
                "line": {
                    "type": "integer",
                    "description": "1-based line number before which content is inserted (required for insert_at)"
                },
                "overwrite": {
                    "type": "boolean",
                    "default": false,
                    "description": "Allow overwriting an existing file when operation is create"
                }
            },
            "required": ["path", "operation"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path":         { "type": "string" },
                "operation":    { "type": "string" },
                "lines_written": { "type": "integer" },
                "bytes_written": { "type": "integer" },
                "replacements": { "type": "integer" },
                "message":      { "type": "string" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let path_str = input.get_str("path").unwrap_or("").to_string();
        let operation = input.get_str("operation").unwrap_or("").to_string();

        if path_str.is_empty() {
            return Ok(SkillOutcome::failure("scribe", "path is required"));
        }

        let path = PathBuf::from(&path_str);

        match operation.as_str() {
            "create" => {
                let content = input.get_str("content").unwrap_or("").to_string();
                let overwrite = input.get_bool("overwrite").unwrap_or(false);
                if path.exists() && !overwrite {
                    return Ok(SkillOutcome::failure(
                        "scribe",
                        &format!(
                            "file already exists: {path_str} (set overwrite:true to overwrite)"
                        ),
                    ));
                }
                if let Some(parent) = path.parent() {
                    std::fs::create_dir_all(parent)?;
                }
                let bytes = content.len();
                let lines = content.lines().count();
                std::fs::write(&path, &content)?;
                Ok(SkillOutcome::success(
                    "scribe",
                    json!({
                        "path": path_str,
                        "operation": "create",
                        "bytes_written": bytes,
                        "lines_written": lines,
                        "replacements": 0,
                        "message": format!("created {path_str} ({lines} lines)")
                    }),
                ))
            }

            "replace_str" | "replace_all" => {
                let old = input.get_str("old_string").unwrap_or("").to_string();
                let new = input.get_str("new_string").unwrap_or("").to_string();
                if old.is_empty() {
                    return Ok(SkillOutcome::failure(
                        "scribe",
                        "old_string is required for replace operations",
                    ));
                }
                if !path.exists() {
                    return Ok(SkillOutcome::failure(
                        "scribe",
                        &format!("file not found: {path_str}"),
                    ));
                }
                let original = std::fs::read_to_string(&path)?;
                let (result, replacements) = if operation == "replace_all" {
                    let count = original.matches(&*old).count();
                    (original.replace(&*old, &new), count)
                } else {
                    if original.contains(&*old) {
                        (original.replacen(&*old, &new, 1), 1)
                    } else {
                        return Ok(SkillOutcome::failure(
                            "scribe",
                            &format!("old_string not found in {path_str}"),
                        ));
                    }
                };
                let bytes = result.len();
                let lines = result.lines().count();
                std::fs::write(&path, &result)?;
                Ok(SkillOutcome::success(
                    "scribe",
                    json!({
                        "path": path_str,
                        "operation": operation,
                        "bytes_written": bytes,
                        "lines_written": lines,
                        "replacements": replacements,
                        "message": format!("{replacements} replacement(s) in {path_str}")
                    }),
                ))
            }

            "append" => {
                let content = input.get_str("content").unwrap_or("").to_string();
                use std::io::Write;
                let mut file = std::fs::OpenOptions::new()
                    .create(true)
                    .append(true)
                    .open(&path)?;
                file.write_all(content.as_bytes())?;
                let bytes = content.len();
                let lines = content.lines().count();
                Ok(SkillOutcome::success(
                    "scribe",
                    json!({
                        "path": path_str,
                        "operation": "append",
                        "bytes_written": bytes,
                        "lines_written": lines,
                        "replacements": 0,
                        "message": format!("appended {lines} lines to {path_str}")
                    }),
                ))
            }

            "insert_at" => {
                let content = input.get_str("content").unwrap_or("").to_string();
                let line_no = input.get_u64("line").unwrap_or(1) as usize;
                if !path.exists() {
                    return Ok(SkillOutcome::failure(
                        "scribe",
                        &format!("file not found: {path_str}"),
                    ));
                }
                let original = std::fs::read_to_string(&path)?;
                let mut lines: Vec<&str> = original.lines().collect();
                let insert_before = line_no.saturating_sub(1).min(lines.len());
                let new_lines: Vec<&str> = content.lines().collect();
                for (i, l) in new_lines.iter().enumerate() {
                    lines.insert(insert_before + i, l);
                }
                let result = lines.join("\n");
                let bytes = result.len();
                let total_lines = lines.len();
                std::fs::write(&path, result)?;
                Ok(SkillOutcome::success(
                    "scribe",
                    json!({
                        "path": path_str,
                        "operation": "insert_at",
                        "bytes_written": bytes,
                        "lines_written": total_lines,
                        "replacements": 0,
                        "message": format!("inserted {} lines before line {line_no} in {path_str}", new_lines.len())
                    }),
                ))
            }

            "delete" => {
                if !path.exists() {
                    return Ok(SkillOutcome::failure(
                        "scribe",
                        &format!("file not found: {path_str}"),
                    ));
                }
                std::fs::remove_file(&path)?;
                Ok(SkillOutcome::success(
                    "scribe",
                    json!({
                        "path": path_str,
                        "operation": "delete",
                        "bytes_written": 0,
                        "lines_written": 0,
                        "replacements": 0,
                        "message": format!("deleted {path_str}")
                    }),
                ))
            }

            other => Ok(SkillOutcome::failure(
                "scribe",
                &format!("unknown operation: {other}"),
            )),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::invocation::{SkillContext, SkillInput};
    use tempfile::tempdir;

    fn ctx() -> SkillContext {
        SkillContext::new("test", ".")
    }

    fn is_ok(o: &SkillOutcome) -> bool {
        o.result.get("error").and_then(|e| e.as_str()).is_none()
    }

    fn is_err(o: &SkillOutcome) -> bool {
        o.result.get("error").and_then(|e| e.as_str()).is_some()
    }

    #[test]
    fn create_new_file() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("hello.txt");
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "create",
                    "content": "line one\nline two"
                })),
                &ctx(),
            )
            .unwrap();
        assert_eq!(out.result["lines_written"], 2);
        assert!(path.exists());
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "line one\nline two"
        );
    }

    #[test]
    fn create_fails_if_exists_without_overwrite() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("existing.txt");
        std::fs::write(&path, "original").unwrap();
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "create",
                    "content": "new content"
                })),
                &ctx(),
            )
            .unwrap();
        assert!(is_err(&out));
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "original");
    }

    #[test]
    fn create_overwrites_with_flag() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("existing.txt");
        std::fs::write(&path, "original").unwrap();
        let skill = ScribeSkill;
        skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "create",
                    "content": "replaced",
                    "overwrite": true
                })),
                &ctx(),
            )
            .unwrap();
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "replaced");
    }

    #[test]
    fn replace_str_first_occurrence_only() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("rep.txt");
        std::fs::write(&path, "foo foo foo").unwrap();
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "replace_str",
                    "old_string": "foo",
                    "new_string": "bar"
                })),
                &ctx(),
            )
            .unwrap();
        assert_eq!(out.result["replacements"], 1);
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "bar foo foo");
    }

    #[test]
    fn replace_all_replaces_every_occurrence() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("rep.txt");
        std::fs::write(&path, "foo foo foo").unwrap();
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "replace_all",
                    "old_string": "foo",
                    "new_string": "bar"
                })),
                &ctx(),
            )
            .unwrap();
        assert_eq!(out.result["replacements"], 3);
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "bar bar bar");
    }

    #[test]
    fn replace_str_fails_when_not_found() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("rep.txt");
        std::fs::write(&path, "hello world").unwrap();
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "replace_str",
                    "old_string": "notfound",
                    "new_string": "x"
                })),
                &ctx(),
            )
            .unwrap();
        assert!(is_err(&out));
    }

    #[test]
    fn append_adds_to_end() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("app.txt");
        std::fs::write(&path, "line one\n").unwrap();
        let skill = ScribeSkill;
        skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "append",
                    "content": "line two"
                })),
                &ctx(),
            )
            .unwrap();
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "line one\nline two"
        );
    }

    #[test]
    fn insert_at_inserts_before_line() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("ins.txt");
        std::fs::write(&path, "a\nb\nc").unwrap();
        let skill = ScribeSkill;
        skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "insert_at",
                    "content": "X",
                    "line": 2
                })),
                &ctx(),
            )
            .unwrap();
        let result = std::fs::read_to_string(&path).unwrap();
        assert_eq!(result, "a\nX\nb\nc");
    }

    #[test]
    fn delete_removes_file() {
        let dir = tempdir().unwrap();
        let path = dir.path().join("del.txt");
        std::fs::write(&path, "bye").unwrap();
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": path.to_str().unwrap(),
                    "operation": "delete"
                })),
                &ctx(),
            )
            .unwrap();
        assert!(is_ok(&out));
        assert!(!path.exists());
    }

    #[test]
    fn delete_fails_on_missing_file() {
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": "/nonexistent/path/file.txt",
                    "operation": "delete"
                })),
                &ctx(),
            )
            .unwrap();
        assert!(is_err(&out));
    }

    #[test]
    fn unknown_operation_returns_error() {
        let skill = ScribeSkill;
        let out = skill
            .execute(
                SkillInput::new(json!({
                    "path": "any.txt",
                    "operation": "frobnicate"
                })),
                &ctx(),
            )
            .unwrap();
        assert!(is_err(&out));
    }
}
