//! Snippet generation for large files.
//!
//! This module implements partial selection strategies (head/tail/window)
//! for files exceeding embed size limits.

use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

use crate::error::ToolResult;

/// Configuration for snippet generation.
#[derive(Debug, Clone)]
pub struct SnippetConfig {
    /// Number of lines to include from the beginning.
    pub head_lines: usize,
    /// Number of lines to include from the end.
    pub tail_lines: usize,
    /// Maximum total bytes for the snippet.
    pub max_snippet_bytes: usize,
}

impl Default for SnippetConfig {
    fn default() -> Self {
        Self {
            head_lines: 200,
            tail_lines: 200,
            max_snippet_bytes: 128_000, // Half of default max_embed_bytes
        }
    }
}

/// A snippet extracted from a file.
#[derive(Debug, Clone)]
pub struct Snippet {
    /// First portion of the file (head).
    pub head: Option<String>,
    /// Last portion of the file (tail).
    pub tail: Option<String>,
    /// Total size of the original file in bytes.
    pub total_size: u64,
    /// Total number of lines in the original file.
    pub total_lines: usize,
    /// Whether the snippet is truncated (doesn't show full file).
    pub truncated: bool,
}

impl Snippet {
    /// Render the snippet with metadata comments.
    ///
    /// Returns a formatted string with:
    /// - Metadata header showing file info and truncation
    /// - Head content
    /// - Separator (if both head and tail present)
    /// - Tail content
    pub fn render(&self, file_path: &Path) -> String {
        let file_name = file_path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown");

        let mut output = String::new();

        // Metadata header
        output.push_str(&format!("# File: {} (truncated)\n", file_name));
        output.push_str(&format!(
            "# Total size: {:.1} KB, {} lines\n",
            self.total_size as f64 / 1024.0,
            self.total_lines
        ));

        if self.head.is_some() && self.tail.is_some() {
            output.push_str(&format!(
                "# Showing: first {} lines + last {} lines\n\n",
                self.head.as_ref().unwrap().lines().count(),
                self.tail.as_ref().unwrap().lines().count()
            ));
        } else if self.head.is_some() {
            output.push_str(&format!(
                "# Showing: first {} lines\n\n",
                self.head.as_ref().unwrap().lines().count()
            ));
        } else if self.tail.is_some() {
            output.push_str(&format!(
                "# Showing: last {} lines\n\n",
                self.tail.as_ref().unwrap().lines().count()
            ));
        }

        // Head content
        if let Some(ref head) = self.head {
            output.push_str(head);
            if !head.ends_with('\n') {
                output.push('\n');
            }
        }

        // Separator if both head and tail
        if self.head.is_some() && self.tail.is_some() {
            output.push_str("\n# ... (middle content omitted) ...\n\n");
        }

        // Tail content
        if let Some(ref tail) = self.tail {
            output.push_str(tail);
            if !tail.ends_with('\n') {
                output.push('\n');
            }
        }

        output
    }
}

/// Generate a snippet from a file.
///
/// Reads the first N and last N lines from the file, respecting UTF-8
/// boundaries and byte limits.
///
/// # Arguments
///
/// * `path` - Path to the file
/// * `config` - Snippet configuration
///
/// # Returns
///
/// A snippet with head/tail content and metadata.
pub fn generate_snippet(path: &Path, config: &SnippetConfig) -> ToolResult<Snippet> {
    let metadata = std::fs::metadata(path)?;
    let total_size = metadata.len();

    // Read the full file to count lines and extract head/tail
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    let mut all_lines: Vec<String> = Vec::new();
    for line in reader.lines() {
        all_lines.push(line?);
    }

    let total_lines = all_lines.len();

    // Check if file is small enough to not need truncation
    if total_lines <= config.head_lines + config.tail_lines {
        return Ok(Snippet {
            head: Some(all_lines.join("\n")),
            tail: None,
            total_size,
            total_lines,
            truncated: false,
        });
    }

    // Extract head and tail
    let head_content = all_lines
        .iter()
        .take(config.head_lines)
        .cloned()
        .collect::<Vec<_>>()
        .join("\n");

    let tail_content = all_lines
        .iter()
        .skip(all_lines.len().saturating_sub(config.tail_lines))
        .cloned()
        .collect::<Vec<_>>()
        .join("\n");

    // Check if combined snippet exceeds max bytes
    let combined_bytes = head_content.len() + tail_content.len();
    if combined_bytes > config.max_snippet_bytes {
        // Reduce to head only if combined is too large
        let truncated_head = truncate_to_bytes(&head_content, config.max_snippet_bytes);
        Ok(Snippet {
            head: Some(truncated_head),
            tail: None,
            total_size,
            total_lines,
            truncated: true,
        })
    } else {
        Ok(Snippet {
            head: Some(head_content),
            tail: Some(tail_content),
            total_size,
            total_lines,
            truncated: true,
        })
    }
}

/// Truncate a string to fit within a byte limit, respecting UTF-8 boundaries.
fn truncate_to_bytes(s: &str, max_bytes: usize) -> String {
    if s.len() <= max_bytes {
        return s.to_string();
    }

    // Find the largest valid UTF-8 boundary within max_bytes
    let mut boundary = max_bytes;
    while boundary > 0 && !s.is_char_boundary(boundary) {
        boundary -= 1;
    }

    s[..boundary].to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_snippet_config_default() {
        let config = SnippetConfig::default();

        assert_eq!(config.head_lines, 200);
        assert_eq!(config.tail_lines, 200);
        assert_eq!(config.max_snippet_bytes, 128_000);
    }

    #[test]
    fn test_generate_snippet_small_file() {
        let mut temp = NamedTempFile::new().unwrap();
        temp.write_all(b"Line 1\nLine 2\nLine 3\n").unwrap();

        let config = SnippetConfig {
            head_lines: 10,
            tail_lines: 10,
            max_snippet_bytes: 10_000,
        };

        let snippet = generate_snippet(temp.path(), &config).unwrap();

        assert!(!snippet.truncated);
        assert_eq!(snippet.total_lines, 3);
        assert!(snippet.head.is_some());
        assert!(snippet.tail.is_none());
    }

    #[test]
    fn test_generate_snippet_large_file() {
        let mut temp = NamedTempFile::new().unwrap();
        for i in 1..=1000 {
            writeln!(temp, "Line {}", i).unwrap();
        }

        let config = SnippetConfig {
            head_lines: 10,
            tail_lines: 10,
            max_snippet_bytes: 100_000,
        };

        let snippet = generate_snippet(temp.path(), &config).unwrap();

        assert!(snippet.truncated);
        assert_eq!(snippet.total_lines, 1000);
        assert!(snippet.head.is_some());
        assert!(snippet.tail.is_some());

        // Head should have first 10 lines
        let head = snippet.head.unwrap();
        assert!(head.contains("Line 1"));
        assert!(head.contains("Line 10"));
        assert!(!head.contains("Line 11"));

        // Tail should have last 10 lines
        let tail = snippet.tail.unwrap();
        assert!(tail.contains("Line 1000"));
        assert!(tail.contains("Line 991"));
        assert!(!tail.contains("Line 990"));
    }

    #[test]
    fn test_snippet_render() {
        let snippet = Snippet {
            head: Some("Line 1\nLine 2".to_string()),
            tail: Some("Line 99\nLine 100".to_string()),
            total_size: 10_000,
            total_lines: 100,
            truncated: true,
        };

        let rendered = snippet.render(Path::new("/test/file.txt"));

        assert!(rendered.contains("# File: file.txt (truncated)"));
        assert!(rendered.contains("# Total size:"));
        assert!(rendered.contains("100 lines"));
        assert!(rendered.contains("Line 1"));
        assert!(rendered.contains("Line 100"));
        assert!(rendered.contains("... (middle content omitted) ..."));
    }

    #[test]
    fn test_snippet_render_head_only() {
        let snippet = Snippet {
            head: Some("Line 1\nLine 2".to_string()),
            tail: None,
            total_size: 1_000,
            total_lines: 10,
            truncated: true,
        };

        let rendered = snippet.render(Path::new("/test/file.txt"));

        assert!(rendered.contains("Line 1"));
        assert!(!rendered.contains("... (middle content omitted) ..."));
    }

    #[test]
    fn test_truncate_to_bytes_exact() {
        let s = "Hello";
        let truncated = truncate_to_bytes(s, 5);
        assert_eq!(truncated, "Hello");
    }

    #[test]
    fn test_truncate_to_bytes_shorter() {
        let s = "Hello, world!";
        let truncated = truncate_to_bytes(s, 5);
        assert_eq!(truncated, "Hello");
    }

    #[test]
    fn test_truncate_to_bytes_utf8_boundary() {
        let s = "Hello 你好世界";
        // Try to truncate in the middle of a multibyte character
        let truncated = truncate_to_bytes(s, 8);
        // Should truncate before the multibyte char to maintain UTF-8 validity
        assert_eq!(truncated, "Hello ");
    }

    #[test]
    fn test_snippet_exceeds_max_bytes() {
        let mut temp = NamedTempFile::new().unwrap();
        for i in 1..=1000 {
            writeln!(temp, "This is a very long line number {} with lots of text", i).unwrap();
        }

        let config = SnippetConfig {
            head_lines: 500,
            tail_lines: 500,
            max_snippet_bytes: 1_000, // Very small limit
        };

        let snippet = generate_snippet(temp.path(), &config).unwrap();

        assert!(snippet.truncated);
        // Should fall back to head only when combined exceeds max
        assert!(snippet.head.is_some());
        let head = snippet.head.unwrap();
        assert!(head.len() <= config.max_snippet_bytes);
    }
}
