//! Content search (grep) with regex and context lines.
//!
//! This module implements:
//! - Regex-based content search
//! - Context line extraction (before/after)
//! - Result count and byte limits
//! - Binary file detection and skip
//! - Case-insensitive matching

use crate::config::SearchConfig;
use crate::error::{ToolError, ToolResult};
use regex::RegexBuilder;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use walkdir::WalkDir;

/// Request to search file contents with regex.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrepRequest {
    /// Base path to search within.
    pub path: String,

    /// Regex pattern to match.
    pub pattern: String,

    /// Optional glob pattern to filter files.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_pattern: Option<String>,

    /// Case-insensitive matching.
    #[serde(default)]
    pub ignore_case: bool,

    /// Number of context lines before match.
    #[serde(default)]
    pub context_before: u32,

    /// Number of context lines after match.
    #[serde(default)]
    pub context_after: u32,

    /// Optional maximum results (overrides config default).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_results: Option<u32>,
}

/// Response from grep search.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrepResponse {
    /// Matches found in files.
    pub matches: Vec<GrepMatch>,

    /// Whether results were truncated due to limits.
    pub truncated: bool,
}

/// A single grep match.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrepMatch {
    /// Path to the file containing the match.
    pub path: PathBuf,

    /// Line number of the match (1-indexed).
    pub line_number: usize,

    /// The matching line content.
    pub line: String,

    /// Context lines before the match.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub context_before: Vec<String>,

    /// Context lines after the match.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub context_after: Vec<String>,
}

/// Search file contents with regex pattern.
///
/// This implementation:
/// 1. Validates path is within allowed roots
/// 2. Compiles regex pattern
/// 3. Traverses directory tree (optionally filtered by file_pattern)
/// 4. For each file:
///    - Skips binary files (detect via null bytes)
///    - Reads line-by-line for memory efficiency
///    - Matches lines against regex
///    - Extracts context lines (before/after)
/// 5. Enforces result limits:
///    - `max_results` match count
///    - `max_bytes` total payload size
/// 6. Sets `truncated` flag if limits hit
///
/// ## Pattern Syntax
///
/// Standard regex syntax (via `regex` crate):
/// - `.` - Any character (except newline by default)
/// - `.*` - Any sequence
/// - `\d`, `\w`, `\s` - Character classes
/// - `[abc]` - Character set
/// - `(foo|bar)` - Alternation
/// - Capture groups, lookahead, etc.
///
/// ## Context Lines
///
/// - `context_before: N` - Include N lines before each match
/// - `context_after: N` - Include N lines after each match
/// - Useful for understanding match context
///
/// ## Binary File Handling
///
/// - Detects binary files by null byte presence
/// - Skips binary files silently
///
/// ## Error Cases
///
/// - Path outside allowed roots → `ToolError::SandboxViolation`
/// - Invalid regex pattern → `ToolError::InvalidInput`
/// - I/O errors during traversal → `ToolError::Io`
///
/// ## Performance
///
/// - Line-by-line reading for large files
/// - Stops early when limits reached
/// - Skips excluded directories
/// - Skips binary files
///
/// ## Platform Notes
///
/// - Handles CRLF line endings on Windows correctly
/// - Tests with Windows-specific paths
///
/// ## See Also
///
/// - Task spec: `docs/building/04_acp_client/04_tasks_02_tools_and_sandboxing.md#TOOLS-SEARCH-03`
pub async fn grep_search(
    request: GrepRequest,
    config: &SearchConfig,
) -> ToolResult<GrepResponse> {
    use crate::path::blocklist::compile_blocklist;
    use globset::GlobBuilder;
    use std::path::Path;

    // Canonicalize the base path
    let base_path = dunce::canonicalize(Path::new(&request.path)).map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            ToolError::NotFound {
                path: request.path.clone(),
            }
        } else {
            ToolError::Io(e)
        }
    })?;

    // Compile regex pattern
    let regex = RegexBuilder::new(&request.pattern)
        .case_insensitive(request.ignore_case)
        .build()
        .map_err(|e| ToolError::InvalidInput(format!("Invalid regex pattern: {}", e)))?;

    // Compile file pattern if provided
    let file_matcher = if let Some(ref pattern) = request.file_pattern {
        let glob = GlobBuilder::new(pattern)
            .literal_separator(false)
            .build()
            .map_err(|e| ToolError::InvalidInput(format!("Invalid file pattern: {}", e)))?;
        Some(glob.compile_matcher())
    } else {
        None
    };

    // Compile exclude patterns
    let exclude_compiled = if !config.default_exclude_globs.is_empty() {
        Some(compile_blocklist(&config.default_exclude_globs)?)
    } else {
        None
    };

    // Determine max results
    let max_results = request.max_results.unwrap_or(config.max_results);
    let max_bytes = config.max_bytes;

    // Walk the directory tree
    let mut matches = Vec::new();
    let mut total_bytes = 0u64;
    let mut truncated = false;

    for entry in WalkDir::new(&base_path)
        .follow_links(false)
        .into_iter()
        .filter_entry(|e| {
            // Skip excluded directories early
            if let Some(ref exclude) = exclude_compiled {
                if exclude.glob_set().is_match(e.path()) {
                    return false;
                }
            }
            true
        })
    {
        // Check if we've hit the result limit
        if matches.len() >= max_results as usize {
            truncated = true;
            break;
        }

        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        // Skip directories
        if entry.file_type().is_dir() {
            continue;
        }

        let entry_path = entry.path();

        // Check file pattern if specified
        if let Some(ref matcher) = file_matcher {
            if !matcher.is_match(entry_path) {
                continue;
            }
        }

        // Search this file
        match search_file(
            entry_path,
            &regex,
            request.context_before as usize,
            request.context_after as usize,
            max_results - matches.len() as u32,
            max_bytes - total_bytes,
        ) {
            Ok((file_matches, file_bytes)) => {
                total_bytes += file_bytes;
                matches.extend(file_matches);

                // Check limits
                if matches.len() >= max_results as usize || total_bytes >= max_bytes {
                    truncated = true;
                    break;
                }
            }
            Err(_) => continue, // Skip files we can't read
        }
    }

    Ok(GrepResponse { matches, truncated })
}

/// Search a single file for regex matches with context.
fn search_file(
    path: &std::path::Path,
    regex: &regex::Regex,
    context_before: usize,
    context_after: usize,
    max_matches: u32,
    max_bytes: u64,
) -> ToolResult<(Vec<GrepMatch>, u64)> {
    // Open file
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    let mut matches: Vec<GrepMatch> = Vec::new();
    let mut total_bytes = 0u64;

    // Ring buffer for context_before lines
    let mut before_buffer: VecDeque<(usize, String)> = VecDeque::new();
    let mut after_countdown = 0usize;
    let mut after_lines: Vec<String> = Vec::new();
    let mut last_match_line = 0usize;

    for (line_num, line_result) in reader.lines().enumerate() {
        if matches.len() >= max_matches as usize {
            break;
        }

        let line = match line_result {
            Ok(l) => l,
            Err(_) => continue,
        };

        // Check for binary file (null bytes)
        if line.contains('\0') {
            return Ok((vec![], 0)); // Skip binary file
        }

        let line_number = line_num + 1; // 1-indexed

        // If we're collecting after-context lines
        if after_countdown > 0 {
            after_lines.push(line.clone());
            after_countdown -= 1;

            // If we've collected all after lines, attach them to the last match
            if after_countdown == 0 && !matches.is_empty() {
                matches.last_mut().unwrap().context_after = after_lines.clone();
                after_lines.clear();
            }
        }

        // Check if this line matches
        if regex.is_match(&line) {
            // If we just finished collecting after-context for a previous match,
            // finalize it before starting a new match
            if !after_lines.is_empty() && !matches.is_empty() {
                matches.last_mut().unwrap().context_after = after_lines.clone();
                after_lines.clear();
            }

            // Collect before-context from the ring buffer
            let before_lines: Vec<String> = before_buffer
                .iter()
                .filter(|(ln, _)| *ln > last_match_line && *ln < line_number)
                .map(|(_, l)| l.clone())
                .collect();

            let match_bytes = (line.len()
                + before_lines.iter().map(|l| l.len()).sum::<usize>()
                + context_after * 50) as u64; // Approximate

            if total_bytes + match_bytes > max_bytes {
                break;
            }

            total_bytes += match_bytes;

            matches.push(GrepMatch {
                path: path.to_path_buf(),
                line_number,
                line: line.clone(),
                context_before: before_lines,
                context_after: Vec::new(), // Will be filled later
            });

            last_match_line = line_number;

            // Start collecting after-context
            if context_after > 0 {
                after_countdown = context_after;
                after_lines.clear();
            }
        }

        // Update before-context ring buffer
        if context_before > 0 {
            before_buffer.push_back((line_number, line.clone()));
            if before_buffer.len() > context_before {
                before_buffer.pop_front();
            }
        }
    }

    Ok((matches, total_bytes))
}
