//! File type detection and MIME type guessing.
//!
//! This module provides utilities for detecting whether a file is text or binary,
//! and guessing appropriate MIME types based on file extensions.

use std::fs;
use std::io::Read;
use std::path::Path;

use crate::error::ToolResult;

/// Information about a file's type and encoding.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileTypeInfo {
    /// Whether the file appears to be text (not binary).
    pub is_text: bool,
    /// Whether the file appears to be binary (not text).
    pub is_binary: bool,
    /// MIME type guess based on extension and content.
    pub mime_type: Option<String>,
    /// Character set (always "utf-8" for text files in Phase 03).
    pub charset: Option<String>,
}

impl FileTypeInfo {
    /// Create a text file type info.
    pub fn text(mime_type: impl Into<String>) -> Self {
        Self {
            is_text: true,
            is_binary: false,
            mime_type: Some(mime_type.into()),
            charset: Some("utf-8".to_string()),
        }
    }

    /// Create a binary file type info.
    pub fn binary(mime_type: impl Into<String>) -> Self {
        Self {
            is_text: false,
            is_binary: true,
            mime_type: Some(mime_type.into()),
            charset: None,
        }
    }

    /// Create an unknown file type info (defaults to binary).
    pub fn unknown() -> Self {
        Self {
            is_text: false,
            is_binary: true,
            mime_type: Some("application/octet-stream".to_string()),
            charset: None,
        }
    }
}

/// Detect file type based on extension and content analysis.
///
/// This uses a fast path (extension-based detection) followed by optional
/// content sniffing for extensionless files.
///
/// # Arguments
///
/// * `path` - Path to the file to analyze
///
/// # Returns
///
/// File type information including text/binary classification and MIME type.
pub fn detect_file_type(path: &Path) -> ToolResult<FileTypeInfo> {
    // First, try extension-based detection (fast path)
    if let Some(info) = detect_by_extension(path) {
        return Ok(info);
    }

    // For extensionless files, do content sniffing
    detect_by_content(path)
}

/// Detect file type by extension.
///
/// This is the fast path for common file types. Returns None if the extension
/// is unknown or missing.
fn detect_by_extension(path: &Path) -> Option<FileTypeInfo> {
    let ext = path.extension()?.to_str()?.to_lowercase();

    match ext.as_str() {
        // Programming languages
        "rs" => Some(FileTypeInfo::text("text/x-rust")),
        "ts" => Some(FileTypeInfo::text("text/typescript")),
        "tsx" => Some(FileTypeInfo::text("text/tsx")),
        "js" => Some(FileTypeInfo::text("text/javascript")),
        "jsx" => Some(FileTypeInfo::text("text/jsx")),
        "py" => Some(FileTypeInfo::text("text/x-python")),
        "rb" => Some(FileTypeInfo::text("text/x-ruby")),
        "go" => Some(FileTypeInfo::text("text/x-go")),
        "java" => Some(FileTypeInfo::text("text/x-java")),
        "c" => Some(FileTypeInfo::text("text/x-c")),
        "cpp" | "cc" | "cxx" => Some(FileTypeInfo::text("text/x-c++")),
        "h" | "hpp" => Some(FileTypeInfo::text("text/x-c-header")),
        "cs" => Some(FileTypeInfo::text("text/x-csharp")),
        "sh" | "bash" | "zsh" => Some(FileTypeInfo::text("text/x-shellscript")),
        "ps1" => Some(FileTypeInfo::text("text/x-powershell")),

        // Data formats
        "json" => Some(FileTypeInfo::text("application/json")),
        "toml" => Some(FileTypeInfo::text("application/toml")),
        "yaml" | "yml" => Some(FileTypeInfo::text("application/yaml")),
        "xml" => Some(FileTypeInfo::text("application/xml")),
        "csv" => Some(FileTypeInfo::text("text/csv")),

        // Markup
        "html" | "htm" => Some(FileTypeInfo::text("text/html")),
        "css" => Some(FileTypeInfo::text("text/css")),
        "scss" | "sass" => Some(FileTypeInfo::text("text/x-scss")),
        "md" | "markdown" => Some(FileTypeInfo::text("text/markdown")),
        "rst" => Some(FileTypeInfo::text("text/x-rst")),

        // Plain text
        "txt" => Some(FileTypeInfo::text("text/plain")),
        "log" => Some(FileTypeInfo::text("text/plain")),

        // Config files
        "ini" | "cfg" | "conf" => Some(FileTypeInfo::text("text/plain")),
        "env" => Some(FileTypeInfo::text("text/plain")),

        // Binary formats
        "png" => Some(FileTypeInfo::binary("image/png")),
        "jpg" | "jpeg" => Some(FileTypeInfo::binary("image/jpeg")),
        "gif" => Some(FileTypeInfo::binary("image/gif")),
        "webp" => Some(FileTypeInfo::binary("image/webp")),
        "svg" => Some(FileTypeInfo::text("image/svg+xml")), // SVG is text
        "pdf" => Some(FileTypeInfo::binary("application/pdf")),
        "zip" => Some(FileTypeInfo::binary("application/zip")),
        "gz" | "gzip" => Some(FileTypeInfo::binary("application/gzip")),
        "tar" => Some(FileTypeInfo::binary("application/x-tar")),
        "mp3" => Some(FileTypeInfo::binary("audio/mpeg")),
        "mp4" => Some(FileTypeInfo::binary("video/mp4")),
        "exe" | "dll" | "so" | "dylib" => Some(FileTypeInfo::binary("application/octet-stream")),

        // Unknown extension
        _ => None,
    }
}

/// Detect file type by analyzing content.
///
/// Reads the first 8KB of the file and checks for UTF-8 validity and
/// presence of null bytes (indicating binary).
fn detect_by_content(path: &Path) -> ToolResult<FileTypeInfo> {
    // Open file
    let mut file = fs::File::open(path)?;

    // Read first 8KB for analysis
    let mut buffer = vec![0u8; 8192];
    let bytes_read = file.read(&mut buffer)?;
    buffer.truncate(bytes_read);

    // Check for null bytes (strong indicator of binary)
    if buffer.contains(&0) {
        return Ok(FileTypeInfo::binary("application/octet-stream"));
    }

    // Try to validate as UTF-8
    match std::str::from_utf8(&buffer) {
        Ok(_) => Ok(FileTypeInfo::text("text/plain")),
        Err(_) => Ok(FileTypeInfo::binary("application/octet-stream")),
    }
}

/// Validate that a file contains valid UTF-8 text.
///
/// This is used to ensure text files can be safely read and embedded.
///
/// # Arguments
///
/// * `path` - Path to the file to validate
///
/// # Returns
///
/// `Ok(true)` if the file is valid UTF-8, `Ok(false)` if not.
pub fn is_valid_utf8(path: &Path) -> ToolResult<bool> {
    let content = fs::read(path)?;
    Ok(std::str::from_utf8(&content).is_ok())
}

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

    #[test]
    fn test_detect_by_extension_rust() {
        let path = Path::new("test.rs");
        let info = detect_by_extension(path).unwrap();

        assert!(info.is_text);
        assert!(!info.is_binary);
        assert_eq!(info.mime_type, Some("text/x-rust".to_string()));
        assert_eq!(info.charset, Some("utf-8".to_string()));
    }

    #[test]
    fn test_detect_by_extension_typescript() {
        let path = Path::new("test.ts");
        let info = detect_by_extension(path).unwrap();

        assert!(info.is_text);
        assert_eq!(info.mime_type, Some("text/typescript".to_string()));
    }

    #[test]
    fn test_detect_by_extension_json() {
        let path = Path::new("config.json");
        let info = detect_by_extension(path).unwrap();

        assert!(info.is_text);
        assert_eq!(info.mime_type, Some("application/json".to_string()));
    }

    #[test]
    fn test_detect_by_extension_binary_png() {
        let path = Path::new("image.png");
        let info = detect_by_extension(path).unwrap();

        assert!(!info.is_text);
        assert!(info.is_binary);
        assert_eq!(info.mime_type, Some("image/png".to_string()));
        assert_eq!(info.charset, None);
    }

    #[test]
    fn test_detect_by_extension_unknown() {
        let path = Path::new("file.unknownext");
        let info = detect_by_extension(path);

        assert!(info.is_none());
    }

    #[test]
    fn test_detect_by_extension_no_extension() {
        let path = Path::new("Makefile");
        let info = detect_by_extension(path);

        assert!(info.is_none());
    }

    #[test]
    fn test_detect_by_content_text() {
        let mut temp = NamedTempFile::new().unwrap();
        temp.write_all(b"Hello, world!\nThis is a text file.\n")
            .unwrap();

        let info = detect_by_content(temp.path()).unwrap();

        assert!(info.is_text);
        assert!(!info.is_binary);
        assert_eq!(info.mime_type, Some("text/plain".to_string()));
    }

    #[test]
    fn test_detect_by_content_binary_with_nulls() {
        let mut temp = NamedTempFile::new().unwrap();
        temp.write_all(b"\x00\x01\x02\x03binary data").unwrap();

        let info = detect_by_content(temp.path()).unwrap();

        assert!(!info.is_text);
        assert!(info.is_binary);
        assert_eq!(info.mime_type, Some("application/octet-stream".to_string()));
    }

    #[test]
    fn test_detect_by_content_invalid_utf8() {
        let mut temp = NamedTempFile::new().unwrap();
        // Invalid UTF-8 sequence
        temp.write_all(&[0xFF, 0xFE, 0xFD]).unwrap();

        let info = detect_by_content(temp.path()).unwrap();

        assert!(!info.is_text);
        assert!(info.is_binary);
    }

    #[test]
    fn test_detect_file_type_with_extension() {
        let path = Path::new("test.rs");
        // Will use extension-based detection (no file needed)
        let info = detect_by_extension(path).unwrap();

        assert!(info.is_text);
        assert_eq!(info.mime_type, Some("text/x-rust".to_string()));
    }

    #[test]
    fn test_is_valid_utf8_valid() {
        let mut temp = NamedTempFile::new().unwrap();
        temp.write_all("Hello, UTF-8! 你好世界".as_bytes())
            .unwrap();

        let result = is_valid_utf8(temp.path()).unwrap();
        assert!(result);
    }

    #[test]
    fn test_is_valid_utf8_invalid() {
        let mut temp = NamedTempFile::new().unwrap();
        temp.write_all(&[0xFF, 0xFE, 0xFD]).unwrap();

        let result = is_valid_utf8(temp.path()).unwrap();
        assert!(!result);
    }

    #[test]
    fn test_file_type_info_constructors() {
        let text = FileTypeInfo::text("text/plain");
        assert!(text.is_text);
        assert!(!text.is_binary);
        assert_eq!(text.charset, Some("utf-8".to_string()));

        let binary = FileTypeInfo::binary("application/pdf");
        assert!(!binary.is_text);
        assert!(binary.is_binary);
        assert_eq!(binary.charset, None);

        let unknown = FileTypeInfo::unknown();
        assert!(!unknown.is_text);
        assert!(unknown.is_binary);
        assert_eq!(unknown.mime_type, Some("application/octet-stream".to_string()));
    }
}
