use anyhow::Result;
#[cfg(feature = "mcp")]
use schemars::JsonSchema;
use semver::{Version, VersionReq};
use serde::Deserialize;
use serde::Serialize;
use serde_json::Deserializer;
use std::collections::HashMap;
use std::env;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{debug, instrument, warn}; // Import io for ErrorKind

/// Parses a crate spec string (e.g., "serde" or "serde@1.0.1")
/// into a crate name and an optional version string.
pub fn parse_crate_spec(crate_spec: &str) -> (String, Option<String>) {
    if let Some(at_pos) = crate_spec.rfind('@') {
        let name = &crate_spec[..at_pos];
        let version_str = &crate_spec[at_pos + 1..];

        // Validate if the part after '@' is a valid semantic version
        // or a typical version requirement (like "1.0" or ">=1.0, <2.0")
        // For now, let's be strict and expect something Version::parse can handle,
        // or allow common forms like "1", "1.0".
        // A more robust approach might involve VersionReq::parse if we want to support ranges,
        // but for fetching a specific version, Version::parse is often what's needed.
        // If the version string is empty or invalid, we might return None for version
        // or treat the original spec as just a name.
        // For now, if @ is present, we expect a version.
        if !version_str.is_empty() {
            // && Version::parse(version_str).is_ok() {
            // We will return the string as is, the caller (fetcher) can decide
            // if it needs strict semver parsing or can handle tags/aliases.
            // For now, the primary goal is to correctly split by '@'.
            // The original code already had a Version::parse check which seems to have been
            // too restrictive for some inputs (e.g. if a user provides just '1' or '1.0').
            // Let's keep it simple: if @ is there, split. Validation can be downstream.
            (name.to_string(), Some(version_str.to_string()))
        } else {
            // If there's an '@' but nothing after it, treat the whole thing as a name.
            (crate_spec.to_string(), None)
        }
    } else {
        // No '@' found, so the whole string is the name, no version specified.
        (crate_spec.to_string(), None)
    }
}

// Helper structs for deserializing cargo metadata
#[derive(Deserialize, Debug)]
struct Metadata {
    packages: Vec<Package>,
    resolve: Option<Resolve>,
}

#[derive(Deserialize, Debug)]
struct Package {
    name: String,
    version: String,
    id: String, // Package ID, e.g., "serde 1.0.150 (registry+https://github.com/rust-lang/crates.io-index)"
    manifest_path: PathBuf,
}

#[derive(Deserialize, Debug)]
struct Resolve {
    nodes: Vec<ResolveNode>,
}

#[derive(Deserialize, Debug)]
struct ResolveNode {
    id: String, // Package ID
}

/// Finds the source directory for a given crate *dependency* using `cargo metadata`.
///
/// Runs `cargo metadata` from the project's root directory.
/// If `crate_version_option` is Some, it finds the path for that specific version *if it's a dependency*.
/// If `crate_version_option` is None, it finds the path for the version resolved
/// in the current project's `Cargo.lock`.
pub fn find_crate_src_path(
    crate_name: &str,
    crate_version_option: Option<&str>,
) -> Result<PathBuf, String> {
    let mut cmd = Command::new("cargo");
    cmd.arg("metadata").arg("--format-version").arg("1");

    if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
        cmd.current_dir(manifest_dir);
        debug!("Running cargo metadata from manifest dir...");
    } else {
        debug!("CARGO_MANIFEST_DIR not set, running cargo metadata from current dir.");
    }

    let output = cmd
        .output()
        .map_err(|e| format!("Failed to execute cargo metadata: {e}"))?;

    if !output.status.success() {
        return Err(format!(
            "cargo metadata failed: {}\nStderr: {}",
            output.status,
            String::from_utf8_lossy(&output.stderr)
        ));
    }

    let metadata: Metadata = serde_json::from_slice(&output.stdout)
        .map_err(|e| format!("Failed to parse cargo metadata JSON: {e}"))?;

    let target_package = metadata.packages.iter().find(|pkg| {
        if pkg.name != crate_name {
            return false;
        }
        if let Some(requested_version_str) = crate_version_option {
            match VersionReq::parse(requested_version_str) {
                Ok(req) => match Version::parse(&pkg.version) {
                    Ok(ver) => req.matches(&ver),
                    Err(_) => false,
                },
                Err(_) => false,
            }
        } else {
            metadata
                .resolve
                .as_ref()
                .is_some_and(|resolve| resolve.nodes.iter().any(|node| node.id == pkg.id))
        }
    });

    match target_package {
        Some(pkg) => {
            let crate_root = pkg.manifest_path.parent().ok_or_else(|| {
                format!(
                    "Could not get parent directory for manifest path: {}",
                    pkg.manifest_path.display()
                )
            })?;
            debug!("Found crate root via metadata: {}", crate_root.display());
            Ok(crate_root.to_path_buf())
        }
        None => {
            if let Some(version) = crate_version_option {
                Err(format!(
                    "Crate '{crate_name}' version requirement '{version}' not found/matched as a dependency in cargo metadata."
                ))
            } else {
                Err(format!(
                    "Crate '{crate_name}' not found as a dependency in cargo metadata."
                ))
            }
        }
    }
}

// --- Structs for parsing ripgrep JSON output --- //

/// Represents the different types of JSON objects output by ripgrep --json.
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)] // Allow dead code for the enum fields we don't read
#[serde(tag = "type", content = "data")]
pub enum RipgrepOutput {
    #[serde(rename = "begin")]
    Begin(BeginData),
    #[serde(rename = "match")]
    Match(MatchData),
    #[serde(rename = "end")]
    End(EndData),
    #[serde(rename = "summary")]
    Summary(SummaryData),
    // Context, Summary types could be added if needed
}

#[derive(Deserialize, Debug, Clone)]
pub struct MatchData {
    pub path: TextPathOrString,
    pub line_number: u64,
}

#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
pub struct TextData {
    pub text: String,
}

// The path can be either directly a string or a TextData object
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum TextPathOrString {
    Object(TextData),
    String(String),
}

impl TextPathOrString {
    pub fn as_string(&self) -> String {
        match self {
            TextPathOrString::Object(text_data) => text_data.text.clone(),
            TextPathOrString::String(s) => s.clone(),
        }
    }
}

// --- End of ripgrep structs --- //

/// Executes the ripgrep (rg) command with the given query and search path.
/// Returns the stdout as a string if successful, otherwise an error string.
#[instrument(skip(query, search_path))]
pub fn execute_ripgrep(
    query: &str,
    search_path: &Path,
    case_sensitive: bool,
) -> Result<String, String> {
    let mut cmd = Command::new("rg");
    cmd.arg("--json")
        .arg("--color=never")
        .arg("-e")
        .arg(query)
        .arg(search_path);

    if case_sensitive {
        cmd.arg("--case-sensitive");
    } else {
        cmd.arg("--ignore-case");
    }

    // Example for include_pattern (glob):
    // if let Some(include) = &params.include_pattern {
    //     cmd.arg("--glob").arg(include);
    // }

    // Example for exclude_pattern (glob):
    // if let Some(exclude) = &params.exclude_pattern {
    //     cmd.arg("--glob").arg(format!("!{}", exclude)); // Ripgrep uses !prefix for exclusion
    // }

    debug!(command = ?cmd, "Executing ripgrep command");

    let output = cmd.output().map_err(|e| {
        if e.kind() == io::ErrorKind::NotFound {
            "ripgrep (rg) command not found in PATH. Please install ripgrep version 13 or later."
                .to_string()
        } else {
            format!(
                "Failed to execute ripgrep command for path '{}': {}",
                search_path.display(),
                e
            )
        }
    })?;

    let stdout_str = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr_str = String::from_utf8_lossy(&output.stderr).to_string();

    // Ripgrep exit codes:
    // 0: Matches found
    // 1: No matches found
    // 2: Error occurred
    // When --json is used, an exit code of 1 is not a failure for our purposes.
    if !output.status.success() && output.status.code() != Some(1) {
        debug!(status = ?output.status, stderr = %stderr_str, "ripgrep command failed with an unexpected error code");
        return Err(format!(
            "ripgrep command failed with status: {}\nStderr: {}\nQuery: '{}' Path: '{}'",
            output.status,
            stderr_str,
            query,
            search_path.display()
        ));
    }

    debug!(
        stdout_len = stdout_str.len(),
        "ripgrep command execution considered successful (exit code 0 or 1)"
    );
    Ok(stdout_str)
}

/// Parses the JSON output from ripgrep (--json) into a vector of match data.
/// Filters out non-match lines (begin, end, summary).
#[instrument(skip(rg_output))]
pub fn parse_ripgrep_output(rg_output: &str) -> Result<Vec<MatchData>, String> {
    debug!(
        output_len = rg_output.len(),
        "Starting to parse ripgrep output"
    );

    if rg_output.is_empty() {
        debug!("Warning - ripgrep output is empty!");
        return Ok(Vec::new());
    }

    debug!("Parsing ripgrep output as JSON stream:");
    let mut matches = Vec::new();
    let stream = Deserializer::from_str(rg_output).into_iter::<RipgrepOutput>();

    debug!("Setup deserializer for JSON stream");
    let mut item_count = 0;
    let mut match_count = 0;

    for (i, result) in stream.enumerate() {
        item_count += 1;
        match result {
            Ok(RipgrepOutput::Match(match_data)) => {
                match_count += 1;
                // eprintln!(
                //     "DEBUG: Found match at index {}: path={}, line={}",
                //     i,
                //     match_data.path.text.as_deref().unwrap_or("N/A"),
                //     match_data.line_number.unwrap_or(0)
                // );
                debug!(index = i, "Parsed stream item as Match");
                matches.push(match_data);
            }
            Ok(other) => {
                // eprintln!("DEBUG: Found non-match item at index {}: {:?}", i, other);
                debug!(index = i, parsed_type = ?other, "Parsed stream item as non-match type");
            }
            Err(e) => {
                debug!(index = i, error = %e, "Error parsing item in ripgrep JSON stream");
                warn!(
                    index = i,
                    error = %e,
                    "Failed to parse item in ripgrep JSON stream, skipping."
                );
            }
        }
    }

    debug!(
        total_items = item_count,
        matches = match_count,
        "Finished parsing JSON stream"
    );

    debug!(
        match_count = matches.len(),
        "Finished parsing rg output stream"
    );
    Ok(matches)
}

// We will use this struct definition
#[derive(Debug, Clone)]
pub struct FileMatch {
    pub line_number: u64,
}

// Represents the successful output structure when matches are found.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "mcp", derive(JsonSchema))]
pub struct SuccessOutput {
    pub file_path: String,
    pub query: String,
    pub crate_spec: String,
    pub search_path: String,
    pub matches: HashMap<String, Vec<u64>>,
}

/// Aggregates a list of `MatchData` (individual line matches from ripgrep)
/// into a `HashMap` where keys are file paths and values are vectors of `FileMatch` (line numbers).
///
/// This function transforms the flat list of matches from ripgrep into a structure
/// more suitable for grouped display or further processing on a per-file basis.
pub fn aggregate_matches_by_file(matches: Vec<MatchData>) -> HashMap<String, Vec<FileMatch>> {
    let mut aggregated: HashMap<String, Vec<FileMatch>> = HashMap::new();
    for m in matches {
        aggregated
            .entry(m.path.as_string())
            .or_default()
            .push(FileMatch {
                line_number: m.line_number,
            });
    }
    for file_matches in aggregated.values_mut() {
        file_matches.sort_by_key(|fm| fm.line_number);
    }
    debug!(count = aggregated.len(), "Aggregated matches by file");
    aggregated
}

/// Formats ripgrep match data into a JSON string for successful scans.
///
/// # Arguments
/// * `query` - The original regex query used for the scan.
/// * `crate_spec` - The crate specification (e.g., "serde@1.0").
/// * `search_path` - The root directory path where the scan was performed.
/// * `aggregated_matches` - A map of file paths to `FileMatch` vectors, as produced by `aggregate_matches_by_file`.
///
/// # Returns
/// A `Result` containing the formatted JSON string on success, or a `serde_json::Error` on failure.
pub fn format_success_output(
    query: &str,
    crate_spec: &str,
    search_path: &str,
    aggregated_matches: HashMap<String, Vec<FileMatch>>,
) -> Result<String, serde_json::Error> {
    let mut final_matches: HashMap<String, Vec<u64>> = HashMap::new();

    for (file_path, matches) in aggregated_matches {
        let line_numbers: Vec<u64> = matches.iter().map(|fm| fm.line_number).collect();
        if !line_numbers.is_empty() {
            let mut sorted_lines = line_numbers;
            sorted_lines.sort_unstable();
            final_matches.insert(file_path, sorted_lines);
        }
    }

    let output = SuccessOutput {
        file_path: search_path.to_string(),
        query: query.to_string(),
        crate_spec: crate_spec.to_string(),
        search_path: search_path.to_string(),
        matches: final_matches,
    };

    serde_json::to_string_pretty(&output)
}

// --- Structs and Functions for Error Output --- //

#[derive(Serialize, Debug)]
struct ErrorOutput {
    error: String,
}

/// Formats an error message into a JSON string.
///
/// # Arguments
/// * `error_message` - The error message to format.
///
/// # Returns
/// A `Result` containing the formatted JSON string on success, or a `serde_json::Error` on failure.
pub fn format_error_output(error_message: &str) -> Result<String, serde_json::Error> {
    let output = ErrorOutput {
        error: error_message.to_string(),
    };
    serde_json::to_string_pretty(&output)
}

#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)] // NEW - Allow dead code for fields
pub struct BeginData {
    path: TextPathOrString,
}

#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)] // NEW - Allow dead code for fields
pub struct EndData {
    path: TextPathOrString,
    binary_offset: Option<u64>,
    stats: Stats,
}

#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)] // NEW - Allow dead code for fields
pub struct SummaryData {
    elapsed_total: Elapsed,
    stats: Stats,
}

#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)] // NEW - Allow dead code for fields
pub struct Stats {
    elapsed: Elapsed,
    searches: u64,
    searches_with_match: u64,
    bytes_searched: u64,
    bytes_printed: u64,
    matched_lines: u64,
    matches: u64,
}

#[cfg_attr(feature = "mcp", derive(JsonSchema))]
#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)] // NEW - Allow dead code for fields
pub struct Elapsed {
    secs: u64,
    nanos: u32,
    human: String,
}

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

    #[test]
    fn test_parse_crate_spec_simple() {
        assert_eq!(parse_crate_spec("serde"), ("serde".to_string(), None));
    }

    #[test]
    fn test_parse_crate_spec_with_version() {
        assert_eq!(
            parse_crate_spec("serde-1.0.197"),
            ("serde-1.0.197".to_string(), None)
        );
    }

    #[test]
    fn test_parse_crate_spec_with_hyphens_in_name() {
        assert_eq!(
            parse_crate_spec("some-crate-name-0.1.0"),
            ("some-crate-name-0.1.0".to_string(), None)
        );
    }

    // Add more test cases for edge cases like invalid formats if needed

    #[test]
    fn test_parse_ripgrep_output_basic() {
        // Use only the first begin and match lines for simplicity
        let rg_output = r#"{"type":"begin","data":{"path":{"text":"src/utils.rs"}}}
{"type":"match","data":{"path":{"text":"src/utils.rs"},"lines":"REMOVED","line_number":19}}"#;

        let result = parse_ripgrep_output(rg_output);
        // Add a debug print to see the error if it occurs
        if let Err(ref _e) = result {
            // eprintln!("Parsing error: {}", e); // This is test-specific, leave or convert to trace for test context
        }
        assert!(result.is_ok(), "Parsing failed: {:?}", result.err()); // Add error details to assert message
        let matches = result.unwrap();

        // Now expect only 1 match
        assert_eq!(matches.len(), 1);

        // Check first match details
        assert_eq!(matches[0].path.as_string(), "src/utils.rs");
        assert_eq!(matches[0].line_number, 19);
        // Removed assertion for matches[0].lines
    }

    #[test]
    fn test_parse_ripgrep_output_empty() {
        let rg_output = "";
        let result = parse_ripgrep_output(rg_output);
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }

    #[test]
    fn test_parse_ripgrep_output_invalid_json() {
        let rg_output = r#"{"type":"match","data":{"path":{"text":"invalid"}}invalidjson"#;
        let result = parse_ripgrep_output(rg_output);
        // The function now skips invalid items and returns Ok, so we expect Ok(empty_vec)
        assert!(
            result.is_ok(),
            "Expected Ok result even with invalid JSON item"
        );
        assert!(
            result.unwrap().is_empty(),
            "Expected empty match vector when JSON stream has invalid items"
        );
        // Optionally check the specific error message
        // assert!(result.unwrap_err().contains("Failed to parse ripgrep JSON line"));
    }

    #[test]
    fn test_parse_ripgrep_output_only_meta() {
        let rg_output = r#"
{"type":"begin","data":{"path":{"text":"src/utils.rs"}}}
{"type":"end","data":{"path":{"text":"src/utils.rs"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":68875,"human":"0.000069s"},"searches":1,"searches_with_match":0,"bytes_searched":0,"bytes_printed":0,"matched_lines":0,"matches":0}}}
{"data":{"elapsed_total":{"human":"0.003296s","nanos":3296208,"secs":0},"stats":{"bytes_printed":0,"bytes_searched":0,"elapsed":{"human":"0.000069s","nanos":68875,"secs":0},"matched_lines":0,"matches":0,"searches":1,"searches_with_match":0}},"type":"summary"}
        "#;
        let result = parse_ripgrep_output(rg_output);
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }

    #[test]
    fn test_aggregate_matches_empty() {
        let matches = Vec::<MatchData>::new();
        let aggregated = aggregate_matches_by_file(matches);
        assert!(aggregated.is_empty());
    }

    #[test]
    fn test_aggregate_matches_single_file() {
        let matches = vec![
            MatchData {
                path: TextPathOrString::String("src/main.rs".to_string()),
                line_number: 10,
            },
            MatchData {
                path: TextPathOrString::String("src/main.rs".to_string()),
                line_number: 5,
            },
        ];
        let aggregated = aggregate_matches_by_file(matches);
        assert_eq!(aggregated.len(), 1);
        assert!(aggregated.contains_key("src/main.rs"));
        let file_matches = aggregated.get("src/main.rs").unwrap();
        assert_eq!(file_matches.len(), 2);
        // Check if lines are sorted correctly
        assert_eq!(file_matches[0].line_number, 5);
        assert_eq!(file_matches[1].line_number, 10);
    }

    #[test]
    fn test_aggregate_matches_multiple_files() {
        let matches = vec![
            MatchData {
                path: TextPathOrString::String("src/main.rs".to_string()),
                line_number: 1,
            },
            MatchData {
                path: TextPathOrString::String("src/lib.rs".to_string()),
                line_number: 1,
            },
            MatchData {
                path: TextPathOrString::String("src/main.rs".to_string()),
                line_number: 2,
            },
        ];
        let aggregated = aggregate_matches_by_file(matches);
        assert_eq!(aggregated.len(), 2);

        assert!(aggregated.contains_key("src/main.rs"));
        let main_matches = aggregated.get("src/main.rs").unwrap();
        assert_eq!(main_matches.len(), 2);
        assert_eq!(main_matches[0].line_number, 1);
        assert_eq!(main_matches[1].line_number, 2);

        assert!(aggregated.contains_key("src/lib.rs"));
        let lib_matches = aggregated.get("src/lib.rs").unwrap();
        assert_eq!(lib_matches.len(), 1);
        assert_eq!(lib_matches[0].line_number, 1);
    }

    #[test]
    fn test_format_error_output() {
        let error_msg = "Something went wrong";
        let result = format_error_output(error_msg);
        assert!(result.is_ok());
        let json_output = result.unwrap();

        // Basic check: does it look like JSON and contain the error message?
        assert!(json_output.starts_with('{'));
        assert!(json_output.ends_with('}'));
        assert!(json_output.contains("\"error\":"));
        assert!(json_output.contains(error_msg));

        // More robust check: Deserialize back and verify
        #[derive(Deserialize)]
        struct TestError {
            error: String,
        }
        let parsed_back: Result<TestError, _> = serde_json::from_str(&json_output);
        assert!(parsed_back.is_ok());
        assert_eq!(parsed_back.unwrap().error, error_msg);
    }

    #[test]
    fn test_format_success_output_empty() {
        let aggregated_matches = HashMap::<String, Vec<FileMatch>>::new();
        let result =
            format_success_output("query", "crate_spec", "search_path", aggregated_matches);
        assert!(result.is_ok());
        let json_output = result.unwrap();

        // Expecting the full success structure with empty matches map, including file_path
        let expected_json = r#"{
  "file_path": "search_path",
  "query": "query",
  "crate_spec": "crate_spec",
  "search_path": "search_path",
  "matches": {}
}"#;
        // Compare after removing whitespace for robustness
        assert_eq!(
            json_output.replace(['\n', ' '], ""),
            expected_json.replace(['\n', ' '], "")
        );
    }

    #[test]
    fn test_format_success_output_with_data() {
        let mut aggregated_matches = HashMap::<String, Vec<FileMatch>>::new();
        aggregated_matches.insert(
            "src/main.rs".to_string(),
            vec![FileMatch { line_number: 5 }, FileMatch { line_number: 10 }],
        );
        aggregated_matches.insert("src/lib.rs".to_string(), vec![FileMatch { line_number: 1 }]);

        let result =
            format_success_output("query", "crate_spec", "search_path", aggregated_matches);
        assert!(result.is_ok());
        let json_output = result.unwrap();

        // Deserialize back to check structure and content
        #[derive(Deserialize, PartialEq, Debug)]
        struct TestSuccess {
            query: String,
            crate_spec: String,
            search_path: String,
            matches: HashMap<String, Vec<u64>>,
        }

        let parsed_back: Result<TestSuccess, _> = serde_json::from_str(&json_output);
        assert!(parsed_back.is_ok());
        let parsed_data = parsed_back.unwrap();

        assert_eq!(parsed_data.query, "query");
        assert_eq!(parsed_data.crate_spec, "crate_spec");
        assert_eq!(parsed_data.search_path, "search_path");
        assert_eq!(parsed_data.matches.len(), 2);
        assert!(parsed_data.matches.contains_key("src/main.rs"));
        assert_eq!(parsed_data.matches["src/main.rs"], vec![5, 10]); // Should be sorted
        assert!(parsed_data.matches.contains_key("src/lib.rs"));
        assert_eq!(parsed_data.matches["src/lib.rs"], vec![1]);
    }
}
