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

/// Flux: surface what changed in a file since the last git commit (HEAD baseline).
/// Returns a structured diff with added/removed lines and context — no noise.
pub struct FluxSkill;

impl Skill for FluxSkill {
    fn id(&self) -> SkillId {
        SkillId("flux")
    }
    fn display_name(&self) -> &str {
        "Flux"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "File path to diff (relative or absolute)"
                },
                "baseline": {
                    "type": "string",
                    "enum": ["HEAD", "staged", "index"],
                    "default": "HEAD",
                    "description": "HEAD: working tree vs last commit. staged: working tree vs staging area. index: staging area vs last commit."
                },
                "context_lines": {
                    "type": "integer",
                    "default": 3,
                    "description": "Lines of unchanged context around each change hunk"
                },
                "compact": {
                    "type": "boolean",
                    "default": false,
                    "description": "Return only added/removed lines without context lines"
                }
            },
            "required": ["path"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path":           { "type": "string" },
                "baseline":       { "type": "string" },
                "added_count":    { "type": "integer" },
                "removed_count":  { "type": "integer" },
                "hunks":          { "type": "array" },
                "diff":           { "type": "string" },
                "unchanged":      { "type": "boolean" }
            }
        })
    }

    fn execute(&self, input: SkillInput, _ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let path = input.get_str("path").unwrap_or("").to_string();
        if path.is_empty() {
            return Ok(SkillOutcome::failure("flux", "path is required"));
        }

        let baseline = input.get_str("baseline").unwrap_or("HEAD").to_string();
        let context_lines = input.get_u64("context_lines").unwrap_or(3);
        let compact = input.get_bool("compact").unwrap_or(false);

        // Build git diff args
        let mut args: Vec<String> = vec!["diff".into()];
        args.push(format!("-U{context_lines}"));

        match baseline.as_str() {
            "staged" => args.push("--diff-filter=M".into()),
            "index" => args.push("--cached".into()),
            _ => {} // HEAD is the default: working tree vs HEAD
        }

        args.push("--".into());
        args.push(path.clone());

        // Run git in the file's containing directory so it finds the right repo
        let work_dir = PathBuf::from(&path)
            .parent()
            .map(|p| p.to_path_buf())
            .unwrap_or_else(|| PathBuf::from("."));

        let output = std::process::Command::new("git")
            .args(&args)
            .current_dir(&work_dir)
            .output();

        let output = match output {
            Ok(o) => o,
            Err(e) => {
                return Ok(SkillOutcome::failure(
                    "flux",
                    &format!("git not available: {e}"),
                ));
            }
        };

        let diff_raw = String::from_utf8_lossy(&output.stdout).to_string();

        if diff_raw.trim().is_empty() {
            return Ok(SkillOutcome::success(
                "flux",
                json!({
                    "path": path,
                    "baseline": baseline,
                    "added_count": 0,
                    "removed_count": 0,
                    "hunks": [],
                    "diff": "",
                    "unchanged": true
                }),
            ));
        }

        let (hunks, added_count, removed_count) = parse_diff(&diff_raw);

        let diff_out = if compact {
            compact_diff(&diff_raw)
        } else {
            diff_raw.clone()
        };

        Ok(SkillOutcome::success(
            "flux",
            json!({
                "path": path,
                "baseline": baseline,
                "added_count": added_count,
                "removed_count": removed_count,
                "hunks": hunks,
                "diff": diff_out,
                "unchanged": false
            }),
        ))
    }
}

fn parse_diff(diff: &str) -> (Vec<serde_json::Value>, usize, usize) {
    let mut hunks = Vec::new();
    let mut added = 0usize;
    let mut removed = 0usize;
    let mut current_hunk: Option<(String, Vec<String>)> = None;

    for line in diff.lines() {
        if line.starts_with("@@") {
            if let Some((header, lines)) = current_hunk.take() {
                hunks.push(json!({ "header": header, "lines": lines }));
            }
            current_hunk = Some((line.to_string(), Vec::new()));
        } else if let Some((_, ref mut lines)) = current_hunk {
            if line.starts_with('+') && !line.starts_with("+++") {
                added += 1;
                lines.push(line.to_string());
            } else if line.starts_with('-') && !line.starts_with("---") {
                removed += 1;
                lines.push(line.to_string());
            } else if !line.starts_with("\\") {
                lines.push(line.to_string());
            }
        }
    }

    if let Some((header, lines)) = current_hunk {
        hunks.push(json!({ "header": header, "lines": lines }));
    }

    (hunks, added, removed)
}

fn compact_diff(diff: &str) -> String {
    diff.lines()
        .filter(|l| l.starts_with('+') || l.starts_with('-') || l.starts_with("@@"))
        .filter(|l| !l.starts_with("+++") && !l.starts_with("---"))
        .collect::<Vec<_>>()
        .join("\n")
}

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

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

    #[test]
    fn parse_diff_counts_added_removed() {
        let diff = "@@ -1,3 +1,4 @@\n context\n-old line\n+new line\n+extra line\n context";
        let (hunks, added, removed) = parse_diff(diff);
        assert_eq!(added, 2);
        assert_eq!(removed, 1);
        assert_eq!(hunks.len(), 1);
    }

    #[test]
    fn parse_diff_empty_returns_zero() {
        let (hunks, added, removed) = parse_diff("");
        assert_eq!(added, 0);
        assert_eq!(removed, 0);
        assert!(hunks.is_empty());
    }

    #[test]
    fn compact_diff_keeps_only_changes() {
        let diff = "@@ -1,2 +1,2 @@\n ctx\n-old\n+new";
        let out = compact_diff(diff);
        assert!(out.contains("-old"));
        assert!(out.contains("+new"));
        assert!(out.contains("@@"));
        assert!(!out.contains("ctx"));
    }

    #[test]
    fn compact_diff_strips_plus_plus_minus_minus() {
        let diff = "--- a/foo.rs\n+++ b/foo.rs\n@@ -1 +1 @@\n-old\n+new";
        let out = compact_diff(diff);
        assert!(!out.contains("--- a"));
        assert!(!out.contains("+++ b"));
    }

    #[test]
    fn parse_diff_multiple_hunks() {
        let diff = "@@ -1,2 +1,2 @@\n-a\n+b\n@@ -10,2 +10,2 @@\n-c\n+d";
        let (hunks, added, removed) = parse_diff(diff);
        assert_eq!(hunks.len(), 2);
        assert_eq!(added, 2);
        assert_eq!(removed, 2);
    }

    #[test]
    fn missing_path_returns_failure() {
        let skill = FluxSkill;
        let out = skill
            .execute(SkillInput::new(serde_json::json!({})), &ctx())
            .unwrap();
        assert!(out.result["error"].as_str().is_some());
    }

    #[test]
    fn baseline_defaults_to_head() {
        let schema = FluxSkill.input_schema();
        assert_eq!(schema["properties"]["baseline"]["default"], "HEAD");
    }

    #[test]
    fn compact_flag_exists_in_schema() {
        let schema = FluxSkill.input_schema();
        assert!(schema["properties"]["compact"].is_object());
    }

    #[test]
    fn context_lines_default_is_3() {
        let schema = FluxSkill.input_schema();
        assert_eq!(schema["properties"]["context_lines"]["default"], 3);
    }

    #[test]
    fn execute_detects_changes_in_real_git_repo() {
        use std::process::Command;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        let path = dir.path();

        // Init a bare git repo
        Command::new("git")
            .arg("init")
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(path)
            .output()
            .unwrap();

        let file = path.join("src.rs");
        std::fs::write(&file, "fn foo() {}\n").unwrap();

        Command::new("git")
            .args(["add", "."])
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(path)
            .output()
            .unwrap();

        // Modify the file
        std::fs::write(&file, "fn foo() {}\nfn bar() {}\n").unwrap();

        let skill = FluxSkill;
        let out = skill
            .execute(
                SkillInput::new(serde_json::json!({ "path": file.to_str().unwrap() })),
                &ctx(),
            )
            .unwrap();

        assert!(
            out.result["error"].as_str().is_none(),
            "unexpected error: {:?}",
            out.result
        );
        assert_eq!(out.result["unchanged"], false);
        assert!(out.result["added_count"].as_u64().unwrap_or(0) >= 1);
        assert_eq!(out.result["removed_count"].as_u64().unwrap_or(0), 0);
    }

    #[test]
    fn execute_unchanged_file_reports_unchanged() {
        use std::process::Command;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        let path = dir.path();

        Command::new("git")
            .arg("init")
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(path)
            .output()
            .unwrap();

        let file = path.join("unchanged.rs");
        std::fs::write(&file, "fn foo() {}\n").unwrap();

        Command::new("git")
            .args(["add", "."])
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(path)
            .output()
            .unwrap();

        // No further modifications
        let skill = FluxSkill;
        let out = skill
            .execute(
                SkillInput::new(serde_json::json!({ "path": file.to_str().unwrap() })),
                &ctx(),
            )
            .unwrap();

        assert_eq!(out.result["unchanged"], true);
        assert_eq!(out.result["added_count"], 0);
    }

    #[test]
    fn execute_compact_mode_strips_context() {
        use std::process::Command;
        use tempfile::tempdir;

        let dir = tempdir().unwrap();
        let path = dir.path();

        Command::new("git")
            .arg("init")
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(path)
            .output()
            .unwrap();

        let file = path.join("compact.rs");
        std::fs::write(&file, "line1\nline2\nline3\nline4\nline5\n").unwrap();
        Command::new("git")
            .args(["add", "."])
            .current_dir(path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(path)
            .output()
            .unwrap();

        std::fs::write(&file, "line1\nline2\nCHANGED\nline4\nline5\n").unwrap();

        let skill = FluxSkill;
        let full_out = skill
            .execute(
                SkillInput::new(serde_json::json!({ "path": file.to_str().unwrap() })),
                &ctx(),
            )
            .unwrap();
        let compact_out = skill
            .execute(
                SkillInput::new(
                    serde_json::json!({ "path": file.to_str().unwrap(), "compact": true }),
                ),
                &ctx(),
            )
            .unwrap();

        let full_diff = full_out.result["diff"].as_str().unwrap_or("");
        let compact_diff = compact_out.result["diff"].as_str().unwrap_or("");
        // Compact must be shorter (context lines removed)
        assert!(
            compact_diff.len() < full_diff.len(),
            "compact should be shorter than full diff"
        );
        // Compact must still contain the changed line
        assert!(compact_diff.contains("CHANGED"));
    }
}
