//! File system operations for finding and renaming files.
//! 
//! All operations are async and non-blocking, suitable for GUI applications.

use std::path::{Path, PathBuf};
use crate::history::RenameAction;
use thiserror::Error;
use tokio::fs;

/// Errors that can occur during file operations.
#[derive(Error, Debug)]
pub enum FrenError {
    #[error("File system error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Invalid glob pattern: {0}")]
    Pattern(String),
    #[error("Regex error: {0}")]
    Regex(#[from] regex::Error),
    #[error("Pattern application error: {0}")]
    PatternApplication(String),
}

/// Represents a single file rename operation.
#[derive(Debug, Clone)]
pub struct FileRename {
    /// Original file path
    pub old_path: PathBuf,
    /// New file path after rename
    pub new_path: PathBuf,
    /// New filename (without path)
    pub new_name: String,
}

/// Result of executing rename operations.
#[derive(Debug)]
pub struct RenameExecutionResult {
    /// Successfully renamed files
    pub successful: Vec<RenameAction>,
    /// Files that were skipped with reasons
    pub skipped: Vec<(PathBuf, String)>,
    /// Files that failed to rename with error messages
    pub errors: Vec<(PathBuf, String)>,
}

/// Finds files matching a glob pattern (non-recursive).
/// 
/// This is a convenience wrapper around `find_matching_files_recursive`
/// with `recursive=false`.
/// 
/// # Arguments
/// 
/// * `pattern` - Glob pattern (e.g., "*.txt", "file?.jpg")
/// 
/// # Returns
/// 
/// * `Ok(Vec<PathBuf>)` - List of matching file paths
/// * `Err(FrenError)` - If pattern is invalid or I/O error occurs
/// 
/// # Examples
/// 
/// ```no_run
/// # tokio_test::block_on(async {
/// use freneng::find_matching_files;
/// 
/// let files = find_matching_files("*.txt").await.unwrap();
/// # })
/// ```
pub async fn find_matching_files(pattern: &str) -> Result<Vec<PathBuf>, FrenError> {
    find_matching_files_recursive(pattern, false).await
}

/// Finds files matching a glob pattern, optionally recursively.
/// 
/// If `recursive=true` and the pattern doesn't contain `**`, it automatically
/// converts patterns like `*.txt` to `**/*.txt` for recursive searching.
/// 
/// Uses async file system operations for non-blocking I/O, suitable for GUI apps.
/// 
/// # Arguments
/// 
/// * `pattern` - Glob pattern (e.g., "*.txt", "**/*.jpg", "./src/**/*.rs")
/// * `recursive` - Whether to search subdirectories
/// 
/// # Returns
/// 
/// * `Ok(Vec<PathBuf>)` - List of matching file paths
/// * `Err(FrenError)` - If pattern is invalid or I/O error occurs
/// 
/// # Examples
/// 
/// ```
/// # tokio_test::block_on(async {
/// use freneng::find_matching_files_recursive;
/// 
/// // Non-recursive
/// let files = find_matching_files_recursive("*.txt", false).await.unwrap();
/// 
/// // Recursive (auto-converts to **/*.txt)
/// let files = find_matching_files_recursive("*.txt", true).await.unwrap();
/// # })
/// ```
pub async fn find_matching_files_recursive(pattern: &str, recursive: bool) -> Result<Vec<PathBuf>, FrenError> {
    use regex::Regex;
    
    // Check if pattern contains glob characters
    let has_glob_chars = pattern.contains('*') || pattern.contains('?') || pattern.contains('[') || pattern.contains(']');
    
    // If no glob characters, check if it's a literal file path
    let pattern = if !has_glob_chars {
        let path = PathBuf::from(pattern);
        let metadata = fs::metadata(&path).await;
        
        if let Ok(meta) = metadata {
            if meta.is_file() {
                // It's a literal file path - return it directly
                return Ok(vec![path]);
            } else if meta.is_dir() && recursive {
                // It's a directory and recursive is enabled - convert to recursive pattern
                // This will be handled by the normal glob matching flow below
                format!("{}/**/*", pattern)
            } else if meta.is_dir() {
                // Directory but not recursive - return empty (directories aren't files)
                return Ok(Vec::new());
            } else {
                // Unknown file type - fall through to glob pattern matching
                pattern.to_string()
            }
        } else {
            // Path doesn't exist - fall through to glob pattern matching
            // (might be a pattern that doesn't match anything yet, or a typo)
            pattern.to_string()
        }
    } else {
        pattern.to_string()
    };
    
    let is_hidden_pattern = pattern.starts_with('.') || pattern.contains("/.");
    
    // Determine base directory to search and normalize pattern
    let (base_dir, normalized_pattern) = if pattern.contains("**") {
        // Pattern contains **, need to find the base directory before **
        if let Some(star_pos) = pattern.find("**") {
            if star_pos == 0 {
                // Pattern starts with **/, base is current dir
                (PathBuf::from("."), pattern.to_string())
            } else {
                // Extract base directory before **
                let before_star = &pattern[..star_pos];
                // Remove trailing slash if present
                let before_star = before_star.trim_end_matches('/');
                if before_star.is_empty() {
                    (PathBuf::from("."), pattern.to_string())
                } else {
                    let base_path = PathBuf::from(before_star);
                    if base_path.is_absolute() {
                        // For absolute paths with **, use the base dir and normalize pattern
                        // Pattern like /tmp/xxx/**/*.txt becomes **/*.txt relative to /tmp/xxx
                        let pattern_after_base = &pattern[star_pos..];
                        (base_path, pattern_after_base.to_string())
                    } else {
                        (PathBuf::from("."), pattern.to_string())
                    }
                }
            }
        } else {
            (PathBuf::from("."), pattern.to_string())
        }
    } else if let Some(slash_pos) = pattern.rfind('/') {
        let base = &pattern[..slash_pos];
        let file_part = &pattern[slash_pos + 1..];
        
        // Handle absolute paths
        if pattern.starts_with('/') {
            let base_path = PathBuf::from(base);
            if base_path.is_absolute() {
                // For absolute paths, use the base as-is and pattern as file part
                (base_path, file_part.to_string())
            } else {
                (PathBuf::from(if base.is_empty() { "." } else { base }), file_part.to_string())
            }
        } else {
            (PathBuf::from(if base.is_empty() { "." } else { base }), file_part.to_string())
        }
    } else {
        // No slash, pattern is just filename
        (PathBuf::from("."), pattern.to_string())
    };
    
    // Convert normalized pattern to regex
    let regex_pattern = glob_to_regex(&normalized_pattern, recursive)?;
    let re = Regex::new(&regex_pattern)
        .map_err(|e| FrenError::Pattern(format!("Invalid pattern: {}", e)))?;
    
    let files = if recursive || pattern.contains("**") {
        // Recursive search
        find_files_recursive_async(&base_dir, &re, is_hidden_pattern).await?
    } else {
        // Non-recursive: only search base directory
        find_files_in_dir_async(&base_dir, &re, is_hidden_pattern).await?
    };
    
    Ok(files)
}

/// Converts a glob pattern to a regex pattern.
fn glob_to_regex(pattern: &str, recursive: bool) -> Result<String, FrenError> {
    let mut regex = String::new();
    regex.push('^');
    
    let mut pattern = pattern.to_string();
    
    // Auto-convert to recursive pattern if needed
    if recursive && !pattern.contains("**") {
        if pattern.starts_with("./") {
            pattern = format!("./**/{}", &pattern[2..]);
        } else if pattern.starts_with('/') {
            pattern = format!("/**{}", pattern);
        } else {
            pattern = format!("**/{}", pattern);
        }
    }
    
    let chars: Vec<char> = pattern.chars().collect();
    let mut i = 0;
    
    while i < chars.len() {
        match chars[i] {
            '*' => {
                if i + 1 < chars.len() && chars[i + 1] == '*' {
                    // ** matches any directory (including zero directories for root files)
                    // If followed by /, match any path; if at end, match anything
                    if i + 2 < chars.len() && chars[i + 2] == '/' {
                        regex.push_str(".*"); // Match any path including root
                        i += 3; // Skip **/
                    } else {
                        regex.push_str(".*"); // Match anything
                        i += 2;
                    }
                } else {
                    // * matches any characters except /
                    regex.push_str("[^/]*");
                    i += 1;
                }
            }
            '?' => {
                regex.push_str("[^/]");
                i += 1;
            }
            '.' => {
                regex.push_str("\\.");
                i += 1;
            }
            '/' => {
                regex.push('/');
                i += 1;
            }
            '[' => {
                regex.push('[');
                i += 1;
                // Handle character class
                while i < chars.len() && chars[i] != ']' {
                    regex.push(chars[i]);
                    i += 1;
                }
                if i < chars.len() {
                    regex.push(']');
                    i += 1;
                }
            }
            c => {
                if c.is_alphanumeric() || c == '_' || c == '-' {
                    regex.push(c);
                } else {
                    regex.push('\\');
                    regex.push(c);
                }
                i += 1;
            }
        }
    }
    
    regex.push('$');
    Ok(regex)
}

/// Recursively finds files matching the regex pattern.
async fn find_files_recursive_async(
    dir: &Path,
    pattern: &regex::Regex,
    include_hidden: bool,
) -> Result<Vec<PathBuf>, FrenError> {
    let mut files = Vec::new();
    // Canonicalize base directory for proper path stripping
    let base_dir = fs::canonicalize(dir).await.unwrap_or_else(|_| dir.to_path_buf());
    let mut dirs_to_search = vec![base_dir.clone()];
    
    while let Some(current_dir) = dirs_to_search.pop() {
        let mut entries = fs::read_dir(&current_dir).await?;
        
        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();
            let metadata = fs::metadata(&path).await?;
            
            if metadata.is_file() {
                // Match against relative path from base directory
                let path_str = if let Ok(rel_path) = path.strip_prefix(&base_dir) {
                    // Use forward slashes for consistency
                    rel_path.to_string_lossy().replace('\\', "/")
                } else {
                    // Fallback: try to make relative to current working directory
                    if let Ok(cwd) = std::env::current_dir() {
                        if let Ok(cwd_canon) = fs::canonicalize(&cwd).await {
                            if let Ok(rel_path) = path.strip_prefix(&cwd_canon) {
                                rel_path.to_string_lossy().replace('\\', "/")
                            } else {
                                path.to_string_lossy().replace('\\', "/")
                            }
                        } else {
                            path.to_string_lossy().replace('\\', "/")
                        }
                    } else {
                        path.to_string_lossy().replace('\\', "/")
                    }
                };
                if pattern.is_match(&path_str) {
                    let is_hidden = path.file_name()
                        .and_then(|n| n.to_str())
                        .map(|s| s.starts_with('.'))
                        .unwrap_or(false);
                    
                    if include_hidden || !is_hidden {
                        files.push(path);
                    }
                }
            } else if metadata.is_dir() {
                // Skip hidden directories unless pattern includes them
                let is_hidden = path.file_name()
                    .and_then(|n| n.to_str())
                    .map(|s| s.starts_with('.'))
                    .unwrap_or(false);
                
                if include_hidden || !is_hidden {
                    // Canonicalize subdirectory paths for consistent path stripping
                    if let Ok(canon_path) = fs::canonicalize(&path).await {
                        dirs_to_search.push(canon_path);
                    } else {
                        dirs_to_search.push(path);
                    }
                }
            }
        }
    }
    
    Ok(files)
}

/// Finds files in a single directory matching the regex pattern.
async fn find_files_in_dir_async(
    dir: &Path,
    pattern: &regex::Regex,
    include_hidden: bool,
) -> Result<Vec<PathBuf>, FrenError> {
    let mut files = Vec::new();
    // Canonicalize base directory for proper path stripping
    let base_dir = fs::canonicalize(dir).await.unwrap_or_else(|_| dir.to_path_buf());
    let mut entries = fs::read_dir(&base_dir).await?;
    
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        let metadata = fs::metadata(&path).await?;
        
        if metadata.is_file() {
            // Match against relative path from base directory
            let path_str = if let Ok(rel_path) = path.strip_prefix(&base_dir) {
                rel_path.to_string_lossy().replace('\\', "/")
            } else {
                path.to_string_lossy().replace('\\', "/")
            };
            if pattern.is_match(&path_str) {
                let is_hidden = path.file_name()
                    .and_then(|n| n.to_str())
                    .map(|s| s.starts_with('.'))
                    .unwrap_or(false);
                
                if include_hidden || !is_hidden {
                    files.push(path);
                }
            }
        }
    }
    
    Ok(files)
}

/// Performs the actual file rename operations asynchronously.
/// 
/// This function executes the rename operations, skipping files that:
/// - Have identical old and new names
/// - Would overwrite existing files (unless `overwrite=true`)
/// 
/// All operations are non-blocking and suitable for GUI applications.
/// 
/// # Arguments
/// 
/// * `renames` - List of rename operations to perform
/// * `overwrite` - Whether to overwrite existing files
/// 
/// # Returns
/// 
/// * `Ok(RenameExecutionResult)` - Results with successful, skipped, and failed renames
/// * `Err(FrenError)` - If a critical error occurs (rare, most errors are in result.errors)
/// 
/// # Examples
/// 
/// ```
/// # tokio_test::block_on(async {
/// use freneng::{perform_renames, FileRename};
/// use std::path::PathBuf;
/// 
/// let renames = vec![FileRename {
///     old_path: PathBuf::from("old.txt"),
///     new_path: PathBuf::from("new.txt"),
///     new_name: "new.txt".to_string(),
/// }];
/// let result = perform_renames(&renames, false).await.unwrap();
/// # })
/// ```
pub async fn perform_renames(
    renames: &[FileRename],
    overwrite: bool,
) -> Result<RenameExecutionResult, FrenError> {
    let mut result = RenameExecutionResult {
        successful: Vec::new(),
        skipped: Vec::new(),
        errors: Vec::new(),
    };

    // Process renames concurrently for better performance
    let rename_futures = renames.iter().map(|rename| async move {
        if rename.old_path == rename.new_path {
            return (rename.old_path.clone(), None, Some("New name is identical to old name".to_string()));
        }

        // Check if target exists
        if fs::metadata(&rename.new_path).await.is_ok() && !overwrite {
            return (rename.old_path.clone(), None, Some("Target file already exists".to_string()));
        }

        // Perform the rename
        match fs::rename(&rename.old_path, &rename.new_path).await {
            Ok(_) => {
                let action = RenameAction {
                    old_path: rename.old_path.clone(),
                    new_path: rename.new_path.clone(),
                };
                (rename.old_path.clone(), Some(action), None)
            }
            Err(e) => {
                (rename.old_path.clone(), None, Some(e.to_string()))
            }
        }
    });

    // Collect results
    let results: Vec<_> = futures::future::join_all(rename_futures).await;
    
    for (path, action, issue) in results {
        match (action, issue) {
            (Some(action), None) => result.successful.push(action),
            (None, Some(reason)) if reason.contains("identical") || reason.contains("already exists") => {
                result.skipped.push((path, reason));
            }
            (None, Some(error)) => {
                result.errors.push((path, error));
            }
            _ => {}
        }
    }

    Ok(result)
}
