//! Utility functions for file handling, path security, and encoding
//!
//! Provides common utilities needed by MCP servers: file collection with
//! gitignore support, path security validation, byte offset conversion,
//! and base64 encoding for pagination cursors.

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

/// Collect files recursively from a path, respecting .gitignore
///
/// Uses `ignore` crate to respect:
/// - `.gitignore` files
/// - `.git/info/exclude`
/// - Global git ignore configuration
///
/// # Arguments
///
/// * `path` - File or directory to collect from
///
/// # Returns
///
/// Vector of file paths (directories are excluded)
///
/// # Example
///
/// ```rust,ignore
/// let files = collect_files(Path::new("src"));
/// for file in files {
///     println!("Found: {:?}", file);
/// }
/// ```
pub fn collect_files(path: &Path) -> Vec<PathBuf> {
    use ignore::WalkBuilder;

    let mut files = Vec::new();
    if path.is_file() {
        files.push(path.to_path_buf());
    } else if path.is_dir() {
        let walker = WalkBuilder::new(path)
            .standard_filters(true) // enables .gitignore, .git/info/exclude, global config
            .build();

        for result in walker.flatten() {
            if result.file_type().is_some_and(|ft| ft.is_file()) {
                files.push(result.path().to_path_buf());
            }
        }
    }
    files
}

/// Convert byte offset to line and column numbers
///
/// Useful for converting byte-based spans from parsers to human-readable
/// line/column positions.
///
/// # Arguments
///
/// * `src` - Source text
/// * `byte_idx` - Byte offset into source
///
/// # Returns
///
/// Tuple of (line, column), both 1-indexed
///
/// # Example
///
/// ```rust
/// use mcp_host::utils::byte_to_line_col;
///
/// let src = "hello\nworld";
/// let (line, col) = byte_to_line_col(src, 6);
/// assert_eq!((line, col), (2, 1)); // First char of "world"
/// ```
pub fn byte_to_line_col(src: &str, byte_idx: usize) -> (usize, usize) {
    let mut line = 1;
    let mut col = 1;
    for (i, ch) in src.char_indices() {
        if i == byte_idx {
            return (line, col);
        }
        if ch == '\n' {
            line += 1;
            col = 1;
        } else {
            col += 1;
        }
    }
    (line, col)
}

/// Check if a path is within a base directory (security boundary)
///
/// Prevents path traversal attacks by ensuring paths don't escape the
/// base directory using `..` or absolute paths.
///
/// # Arguments
///
/// * `path` - Path to validate
/// * `base_dir` - Base directory (security boundary)
///
/// # Returns
///
/// `true` if path is within base_dir, `false` otherwise
///
/// # Security
///
/// This function prevents:
/// - Absolute paths outside base_dir
/// - Relative paths with `..` that escape base_dir
/// - Symlink escapes (via canonicalization)
///
/// # Example
///
/// ```rust,ignore
/// let cwd = std::env::current_dir()?;
/// assert!(is_safe_path(Path::new("foo/bar.txt"), &cwd));
/// assert!(!is_safe_path(Path::new("../etc/passwd"), &cwd));
/// ```
pub fn is_safe_path(path: &Path, base_dir: &Path) -> bool {
    let resolved = if path.is_absolute() {
        path.to_path_buf()
    } else {
        base_dir.join(path)
    };

    // Try canonicalize first (handles symlinks and ..)
    if let (Ok(resolved_canon), Ok(base_canon)) = (resolved.canonicalize(), base_dir.canonicalize())
    {
        return resolved_canon.starts_with(&base_canon);
    }

    // For non-existent paths, do basic check:
    // - Absolute paths outside base are rejected
    // - Relative paths with .. that escape are rejected
    if path.is_absolute() {
        if let Ok(base_canon) = base_dir.canonicalize() {
            return resolved.starts_with(&base_canon);
        }
        return false;
    }

    // Relative path - check it doesn't start with .. that would escape
    let mut depth: i32 = 0;
    for component in path.components() {
        match component {
            std::path::Component::ParentDir => depth -= 1,
            std::path::Component::Normal(_) => depth += 1,
            _ => {}
        }
        if depth < 0 {
            return false; // Escapes base with ..
        }
    }
    true // Relative path stays within base
}

/// Base64 encode bytes
///
/// Useful for creating pagination cursors or encoding binary data
/// for JSON transmission.
///
/// # Arguments
///
/// * `data` - Bytes to encode
///
/// # Returns
///
/// Base64-encoded string
pub fn base64_encode(data: &[u8]) -> String {
    use base64::{engine::general_purpose::STANDARD, Engine};
    STANDARD.encode(data)
}

/// Base64 decode string
///
/// Decodes base64-encoded data. Returns empty vector on invalid input.
///
/// # Arguments
///
/// * `s` - Base64-encoded string
///
/// # Returns
///
/// Decoded bytes, or empty vector if invalid
pub fn base64_decode(s: &str) -> Vec<u8> {
    use base64::{engine::general_purpose::STANDARD, Engine};
    STANDARD.decode(s).unwrap_or_default()
}

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

    // ========== is_safe_path tests ==========

    #[test]
    fn test_is_safe_path_relative() {
        let base = std::env::current_dir().unwrap();
        assert!(is_safe_path(Path::new("foo"), &base));
        assert!(is_safe_path(Path::new("foo/bar"), &base));
        assert!(is_safe_path(Path::new("./foo"), &base));
    }

    #[test]
    fn test_is_safe_path_parent_escape() {
        let base = std::env::current_dir().unwrap();
        assert!(!is_safe_path(Path::new("../foo"), &base));
        assert!(!is_safe_path(Path::new("foo/../../bar"), &base));
        assert!(!is_safe_path(Path::new(".."), &base));
    }

    #[test]
    fn test_is_safe_path_absolute_outside() {
        let base = std::env::current_dir().unwrap();
        assert!(!is_safe_path(Path::new("/tmp"), &base));
        assert!(!is_safe_path(Path::new("/etc/passwd"), &base));
        assert!(!is_safe_path(Path::new("/home"), &base));
    }

    // ========== Base64 encoding tests ==========

    #[test]
    fn test_base64_roundtrip() {
        let original = "42";
        let encoded = base64_encode(original.as_bytes());
        let decoded = String::from_utf8(base64_decode(&encoded)).unwrap();
        assert_eq!(original, decoded);
    }

    #[test]
    fn test_base64_offset_encoding() {
        let offsets = [0, 10, 100, 12345];
        for offset in offsets {
            let encoded = base64_encode(offset.to_string().as_bytes());
            let decoded: usize = String::from_utf8(base64_decode(&encoded))
                .unwrap()
                .parse()
                .unwrap();
            assert_eq!(offset, decoded);
        }
    }

    #[test]
    fn test_base64_invalid_input() {
        // Invalid base64 should return empty vec
        let result = base64_decode("!!!invalid!!!");
        assert!(result.is_empty());
    }

    // ========== byte_to_line_col tests ==========

    #[test]
    fn test_byte_to_line_col_start() {
        let src = "hello\nworld\n";
        let (line, col) = byte_to_line_col(src, 0);
        assert_eq!((line, col), (1, 1));
    }

    #[test]
    fn test_byte_to_line_col_middle_first_line() {
        let src = "hello\nworld\n";
        let (line, col) = byte_to_line_col(src, 2);
        assert_eq!((line, col), (1, 3)); // 'l' at position 2
    }

    #[test]
    fn test_byte_to_line_col_second_line() {
        let src = "hello\nworld\n";
        let (line, col) = byte_to_line_col(src, 6);
        assert_eq!((line, col), (2, 1)); // 'w' at start of line 2
    }

    #[test]
    fn test_byte_to_line_col_end() {
        let src = "hello\nworld\n";
        let (line, col) = byte_to_line_col(src, 11);
        assert_eq!((line, col), (2, 6)); // newline at end of 'world'
    }

    #[test]
    fn test_byte_to_line_col_beyond_end() {
        let src = "hi";
        let (line, col) = byte_to_line_col(src, 100);
        // Should return last position
        assert_eq!((line, col), (1, 3));
    }
}
