use std::path::{Path, PathBuf};
use std::time::Duration;

use anyhow::{Context, Result};
use regex::Regex;
use tokio::process::Command;
use tracing::{debug, instrument, warn};

use crate::types::SearchResult;

/// Executes grep searches against the workspace safely, parsing the output
/// into [`SearchResult`] values.
pub struct GrepExecutor {
    /// Root directory to search within. All grep commands are scoped to this
    /// directory.
    workspace_root: PathBuf,
    /// Maximum time a single grep process is allowed to run.
    timeout: Duration,
    /// Maximum number of result lines to accept from a single grep invocation.
    max_results: usize,
}

impl GrepExecutor {
    pub fn new(workspace_root: PathBuf, timeout: Duration, max_results: usize) -> Self {
        Self {
            workspace_root,
            timeout,
            max_results,
        }
    }

    /// Execute a grep search for `pattern` under the workspace root.
    ///
    /// The pattern is sanitised before use to prevent command injection.
    /// Results are returned sorted by file path and line number.
    #[instrument(skip(self), fields(workspace = %self.workspace_root.display()))]
    pub async fn execute(&self, pattern: &str) -> Result<Vec<SearchResult>> {
        let sanitized = Self::sanitize_pattern(pattern);
        if sanitized.is_empty() {
            warn!(original = pattern, "pattern was empty after sanitisation");
            return Ok(Vec::new());
        }

        debug!(pattern = %sanitized, "executing grep");

        let output = tokio::time::timeout(self.timeout, self.run_grep(&sanitized))
            .await
            .context("grep timed out")?
            .context("grep execution failed")?;

        let results = self.parse_grep_output(&output);

        debug!(
            count = results.len(),
            "grep returned results"
        );

        Ok(results)
    }

    /// Execute multiple grep patterns in parallel, collecting all results into
    /// a single vector.
    #[instrument(skip(self, patterns), fields(pattern_count = patterns.len()))]
    pub async fn execute_many(&self, patterns: &[String]) -> Result<Vec<SearchResult>> {
        let handles: Vec<_> = patterns
            .iter()
            .map(|pattern| {
                let executor = GrepExecutor::new(
                    self.workspace_root.clone(),
                    self.timeout,
                    self.max_results,
                );
                let pattern = pattern.clone();
                tokio::spawn(async move { executor.execute(&pattern).await })
            })
            .collect();

        let mut all_results = Vec::new();
        for handle in handles {
            match handle.await {
                Ok(Ok(results)) => all_results.extend(results),
                Ok(Err(e)) => {
                    warn!(error = %e, "grep execution failed for one pattern");
                }
                Err(e) => {
                    warn!(error = %e, "grep task panicked");
                }
            }
        }

        Ok(all_results)
    }

    // ---- internal ----

    /// Run `grep -rn --include='*' <pattern> <workspace_root>`, capturing
    /// stdout. We intentionally avoid shell expansion by passing arguments
    /// directly to the process.
    async fn run_grep(&self, pattern: &str) -> Result<String> {
        let output = Command::new("grep")
            .arg("-rn")                      // recursive + line numbers
            .arg("--color=never")            // no ANSI escape codes
            .arg("-m")
            .arg(self.max_results.to_string()) // per-file match limit
            .arg("--")                       // end of flags
            .arg(pattern)
            .arg(&self.workspace_root)
            // Exclude common non-source directories.
            .arg("--exclude-dir=.git")
            .arg("--exclude-dir=node_modules")
            .arg("--exclude-dir=target")
            .arg("--exclude-dir=vendor")
            .arg("--exclude-dir=dist")
            .arg("--exclude-dir=build")
            .output()
            .await
            .context("failed to spawn grep process")?;

        // grep returns exit code 1 when no matches are found -- that is not
        // an error.
        if !output.status.success() && output.status.code() != Some(1) {
            let stderr = String::from_utf8_lossy(&output.stderr);
            warn!(exit_code = ?output.status.code(), stderr = %stderr, "grep returned an error");
        }

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

    /// Parse the raw grep output (one match per line in the format
    /// `<file>:<line>:<content>`) into [`SearchResult`] values.
    fn parse_grep_output(&self, output: &str) -> Vec<SearchResult> {
        let mut results = Vec::new();
        let workspace_prefix = self.workspace_root.to_string_lossy();

        for line in output.lines().filter(|l| !l.is_empty()).take(self.max_results) {
            if let Some(result) = Self::parse_grep_line(line, &workspace_prefix) {
                results.push(result);
            }
        }

        results
    }

    /// Parse a single grep output line of the form `file:line_no:content`.
    fn parse_grep_line(line: &str, workspace_prefix: &str) -> Option<SearchResult> {
        // Find the first colon (end of file path). Handles Windows-style
        // paths like C:\... by looking for `:digit:` patterns.
        let first_colon = line.find(':')?;
        let rest = &line[first_colon + 1..];

        // The second segment should be the line number.
        let second_colon = rest.find(':')?;
        let line_num_str = &rest[..second_colon];
        let line_number: usize = line_num_str.parse().ok()?;

        let content = rest[second_colon + 1..].to_string();

        let mut file_path = line[..first_colon].to_string();

        // Strip the workspace root prefix so paths are relative.
        if file_path.starts_with(workspace_prefix) {
            file_path = file_path[workspace_prefix.len()..].to_string();
            // Remove leading slash.
            if file_path.starts_with('/') || file_path.starts_with('\\') {
                file_path = file_path[1..].to_string();
            }
        }

        Some(SearchResult {
            file_path,
            content: content.trim().to_string(),
            relevance_score: 0.5, // default score; will be refined by the ranker
            line_number: Some(line_number),
        })
    }

    /// Sanitize a grep pattern to prevent command injection and remove
    /// characters that could cause grep to misbehave.
    ///
    /// We allow alphanumeric characters, basic punctuation used in code
    /// patterns, and common regex meta-characters that are useful for
    /// searching.
    pub fn sanitize_pattern(pattern: &str) -> String {
        // Remove null bytes and control characters (except common whitespace).
        let cleaned: String = pattern
            .chars()
            .filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
            .collect();

        // Remove shell-dangerous characters that could allow injection.
        // We are *not* passing through a shell, but extra caution doesn't hurt.
        let dangerous = Regex::new(r"[`$;|&><!\x00]").expect("static regex");
        let sanitized = dangerous.replace_all(&cleaned, "").to_string();

        // Collapse excessive whitespace.
        let whitespace = Regex::new(r"\s+").expect("static regex");
        let sanitized = whitespace.replace_all(&sanitized, " ").to_string();

        // Limit pattern length to prevent DoS.
        let max_len = 500;
        if sanitized.len() > max_len {
            sanitized[..max_len].to_string()
        } else {
            sanitized.trim().to_string()
        }
    }

    /// Check whether a file path is within the workspace root (prevents path
    /// traversal attacks).
    pub fn is_safe_path(&self, path: &str) -> bool {
        let candidate = if Path::new(path).is_absolute() {
            PathBuf::from(path)
        } else {
            self.workspace_root.join(path)
        };

        // Canonicalization may fail if the file doesn't exist yet, so we do a
        // best-effort prefix check on the raw path components.
        match (candidate.canonicalize(), self.workspace_root.canonicalize()) {
            (Ok(canon), Ok(root)) => canon.starts_with(&root),
            _ => {
                // Fallback: reject paths with `..` segments.
                !path.contains("..")
            }
        }
    }
}

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

    #[test]
    fn test_sanitize_strips_dangerous_chars() {
        let result = GrepExecutor::sanitize_pattern("hello; rm -rf /");
        assert!(!result.contains(';'));
        assert!(result.contains("hello"));
    }

    #[test]
    fn test_sanitize_preserves_regex_chars() {
        let result = GrepExecutor::sanitize_pattern(r"fn\s+\w+\(");
        assert_eq!(result, r"fn\s+\w+\(");
    }

    #[test]
    fn test_sanitize_truncates_long_patterns() {
        let long = "a".repeat(1000);
        let result = GrepExecutor::sanitize_pattern(&long);
        assert_eq!(result.len(), 500);
    }

    #[test]
    fn test_sanitize_empty_input() {
        let result = GrepExecutor::sanitize_pattern("");
        assert!(result.is_empty());
    }

    #[test]
    fn test_parse_grep_line() {
        let line = "/workspace/src/main.rs:42:fn main() {";
        let result = GrepExecutor::parse_grep_line(line, "/workspace");
        assert!(result.is_some());
        let r = result.unwrap();
        assert_eq!(r.file_path, "src/main.rs");
        assert_eq!(r.line_number, Some(42));
        assert_eq!(r.content, "fn main() {");
    }

    #[test]
    fn test_parse_grep_line_no_match() {
        let result = GrepExecutor::parse_grep_line("no colons here", "/workspace");
        assert!(result.is_none());
    }

    #[test]
    fn test_is_safe_path_rejects_traversal() {
        let executor = GrepExecutor::new(
            PathBuf::from("/workspace"),
            Duration::from_secs(10),
            100,
        );
        assert!(!executor.is_safe_path("../../etc/passwd"));
        assert!(executor.is_safe_path("src/main.rs"));
    }
}
