pub mod cache;
pub mod config;
pub mod deps;
pub mod self_update;
pub mod session;

use anyhow::Result;
use cache::ProjectCache;
use config::Config;
use session::{SessionConfig, TmuxSession};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use walkdir::WalkDir;

pub fn show_cache_status(config: &Config) -> Result<()> {
    if !config.cache_enabled {
        println!("Cache is disabled in configuration");
        return Ok(());
    }

    if let Some(cache) = ProjectCache::load()? {
        let age_seconds = cache.age_in_seconds();
        let age_hours = age_seconds / 3600;
        let age_minutes = (age_seconds % 3600) / 60;

        println!("Cache status:");
        println!("  Location: {}", ProjectCache::cache_path()?.display());
        println!("  Projects cached: {}", cache.projects.len());
        println!("  Directories tracked: {}", cache.dir_mtimes.len());
        println!("  Last updated: {}h {}m ago", age_hours, age_minutes);
    } else {
        println!("No cache found");
        println!("  Run without --cache-status to create it");
    }

    Ok(())
}

pub fn get_projects(config: &Config, force_refresh: bool) -> Result<Vec<PathBuf>> {
    get_projects_internal(
        config,
        force_refresh,
        &ProjectCache::load,
        &|cache| cache.save(),
        &scan_all_roots,
    )
}

#[allow(clippy::type_complexity)]
fn get_projects_internal(
    config: &Config,
    force_refresh: bool,
    cache_loader: &dyn Fn() -> Result<Option<ProjectCache>>,
    cache_saver: &dyn Fn(&ProjectCache) -> Result<()>,
    scanner: &dyn Fn(&Config) -> Result<(Vec<PathBuf>, HashMap<PathBuf, u64>)>,
) -> Result<Vec<PathBuf>> {
    if !config.cache_enabled || force_refresh {
        let (projects, fingerprints) = scanner(config)?;
        let cache = ProjectCache::new(projects.clone(), fingerprints);
        cache_saver(&cache)?;
        eprintln!("Cache updated with {} projects", projects.len());
        return Ok(projects);
    }

    if let Some(mut cache) = cache_loader()? {
        // Cache no formato antigo (sem dir_mtimes) → atualizar com rescan completo
        if cache.dir_mtimes.is_empty() {
            eprintln!("Upgrading cache, scanning for projects...");
            let (projects, fingerprints) = scanner(config)?;
            let new_cache = ProjectCache::new(projects.clone(), fingerprints);
            cache_saver(&new_cache)?;
            eprintln!("Cache updated with {} projects", projects.len());
            return Ok(projects);
        }

        let changed = cache.validate_and_update(&|root| scan_from_root(root, config))?;
        if changed {
            cache_saver(&cache)?;
            eprintln!(
                "Cache updated incrementally ({} projects)",
                cache.projects.len()
            );
        } else {
            eprintln!("Using cached projects ({} projects)", cache.projects.len());
        }
        return Ok(cache.projects);
    }

    // Sem cache ainda — scan completo inicial
    eprintln!("No cache found, scanning for projects...");
    let (projects, fingerprints) = scanner(config)?;
    let cache = ProjectCache::new(projects.clone(), fingerprints);
    cache_saver(&cache)?;
    eprintln!("Cache updated with {} projects", projects.len());
    Ok(projects)
}

pub fn scan_all_roots(config: &Config) -> Result<(Vec<PathBuf>, HashMap<PathBuf, u64>)> {
    let mut all_projects = Vec::new();
    let mut all_fingerprints = HashMap::new();

    for path_str in &config.paths {
        let path = PathBuf::from(shellexpand::tilde(path_str).to_string());

        if !path.exists() {
            eprintln!("Warning: Path does not exist: {}", path.display());
            continue;
        }

        eprintln!("Scanning: {}", path.display());

        let (projects, fingerprints) = scan_from_root(&path, config)?;
        all_projects.extend(projects);
        all_fingerprints.extend(fingerprints);
    }

    all_projects.sort();
    all_projects.dedup();

    Ok((all_projects, all_fingerprints))
}

pub fn scan_from_root(
    root: &Path,
    config: &Config,
) -> Result<(Vec<PathBuf>, HashMap<PathBuf, u64>)> {
    let mut projects = Vec::new();
    let mut fingerprints = HashMap::new();

    for entry in WalkDir::new(root)
        .max_depth(config.max_depth)
        .follow_links(false)
        .into_iter()
        .filter_entry(|e| {
            e.file_name()
                .to_str()
                .map(|s| !s.starts_with('.') || s == ".git")
                .unwrap_or(false)
        })
    {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        if entry.file_type().is_dir() {
            if entry.file_name() == ".git" {
                // Projeto encontrado
                if let Some(parent) = entry.path().parent() {
                    projects.push(parent.to_path_buf());
                }
            } else {
                // Registrar mtime para detecção de mudanças futuras
                if let Ok(metadata) = entry.metadata()
                    && let Ok(modified) = metadata.modified()
                {
                    let mtime = modified
                        .duration_since(UNIX_EPOCH)
                        .unwrap_or_default()
                        .as_secs();
                    fingerprints.insert(entry.path().to_path_buf(), mtime);
                }
            }
        }
    }

    Ok((projects, fingerprints))
}

pub fn launch_tmux_session(selected: &Path, config: &Config) -> Result<()> {
    // Try to load project-specific config, fallback to global default
    let session_config = SessionConfig::load_from_project(selected)?
        .unwrap_or_else(|| config.default_session.clone());

    // Create tmux session
    let tmux_session = TmuxSession::new(selected);
    tmux_session.create(&session_config)?;

    Ok(())
}

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

    fn create_test_config(cache_enabled: bool) -> Config {
        Config {
            paths: vec!["/tmp/test".to_string()],
            max_depth: 3,
            cache_enabled,
            cache_ttl_hours: 24,
            default_session: session::SessionConfig { windows: vec![] },
        }
    }

    #[test]
    fn should_scan_when_cache_disabled() {
        let config = create_test_config(false);
        let projects = vec![PathBuf::from("/tmp/test/project1")];
        let fingerprints = HashMap::new();
        let expected_projects = projects.clone();

        let scanner_called = RefCell::new(false);
        let saver_called = RefCell::new(false);

        let result = get_projects_internal(
            &config,
            false,
            &|| panic!("should not load cache when disabled"),
            &|_| {
                *saver_called.borrow_mut() = true;
                Ok(())
            },
            &|_| {
                *scanner_called.borrow_mut() = true;
                Ok((expected_projects.clone(), fingerprints.clone()))
            },
        );

        assert!(result.is_ok());
        assert!(scanner_called.into_inner());
        assert!(saver_called.into_inner());
        assert_eq!(result.unwrap(), projects);
    }

    #[test]
    fn should_scan_when_force_refresh() {
        let config = create_test_config(true);
        let projects = vec![PathBuf::from("/tmp/test/project1")];
        let fingerprints = HashMap::new();
        let expected_projects = projects.clone();

        let scanner_called = RefCell::new(false);
        let saver_called = RefCell::new(false);

        let result = get_projects_internal(
            &config,
            true,
            &|| panic!("should not load cache when force refresh"),
            &|_| {
                *saver_called.borrow_mut() = true;
                Ok(())
            },
            &|_| {
                *scanner_called.borrow_mut() = true;
                Ok((expected_projects.clone(), fingerprints.clone()))
            },
        );

        assert!(result.is_ok());
        assert!(scanner_called.into_inner());
        assert!(saver_called.into_inner());
        assert_eq!(result.unwrap(), projects);
    }

    #[test]
    fn should_do_initial_scan_when_no_cache_exists() {
        let config = create_test_config(true);
        let projects = vec![PathBuf::from("/tmp/test/project1")];
        let fingerprints = HashMap::new();
        let expected_projects = projects.clone();

        let loader_called = RefCell::new(false);
        let scanner_called = RefCell::new(false);
        let saver_called = RefCell::new(false);

        let result = get_projects_internal(
            &config,
            false,
            &|| {
                *loader_called.borrow_mut() = true;
                Ok(None)
            },
            &|_| {
                *saver_called.borrow_mut() = true;
                Ok(())
            },
            &|_| {
                *scanner_called.borrow_mut() = true;
                Ok((expected_projects.clone(), fingerprints.clone()))
            },
        );

        assert!(result.is_ok());
        assert!(loader_called.into_inner());
        assert!(scanner_called.into_inner());
        assert!(saver_called.into_inner());
        assert_eq!(result.unwrap(), projects);
    }

    #[test]
    fn should_upgrade_old_cache_format() {
        let config = create_test_config(true);
        let old_projects = vec![PathBuf::from("/old/project")];
        let new_projects = vec![
            PathBuf::from("/new/project1"),
            PathBuf::from("/new/project2"),
        ];
        let new_fingerprints = HashMap::from([(PathBuf::from("/new"), 12345u64)]);

        // Use RefCell<Option<>> to allow moving into closure multiple times
        let old_cache = RefCell::new(Some(ProjectCache::new(old_projects, HashMap::new())));

        let loader_called = RefCell::new(false);
        let scanner_called = RefCell::new(false);
        let saver_count = RefCell::new(0);

        let result = get_projects_internal(
            &config,
            false,
            &|| {
                *loader_called.borrow_mut() = true;
                // Take the cache out of the RefCell
                Ok(old_cache.borrow_mut().take())
            },
            &|_| {
                *saver_count.borrow_mut() += 1;
                Ok(())
            },
            &|_| {
                *scanner_called.borrow_mut() = true;
                Ok((new_projects.clone(), new_fingerprints.clone()))
            },
        );

        assert!(result.is_ok());
        assert!(loader_called.into_inner());
        assert!(scanner_called.into_inner());
        assert_eq!(*saver_count.borrow(), 1);
        assert_eq!(result.unwrap(), new_projects);
    }

    #[test]
    fn should_use_cached_projects_when_nothing_changed() {
        let config = create_test_config(true);
        let cached_projects = vec![
            PathBuf::from("/nonexistent/project1"),
            PathBuf::from("/nonexistent/project2"),
        ];
        // Use a path that doesn't exist - validate_and_update will skip rescan
        // because it can't check mtime of non-existent directory
        let cached_fingerprints =
            HashMap::from([(PathBuf::from("/definitely_nonexistent_path_xyz"), 12345u64)]);

        // Use RefCell<Option<>> to allow moving into closure multiple times
        let cache = RefCell::new(Some(ProjectCache::new(
            cached_projects.clone(),
            cached_fingerprints,
        )));

        let loader_called = RefCell::new(false);
        let scanner_called = RefCell::new(false);
        let saver_count = RefCell::new(0);

        let result = get_projects_internal(
            &config,
            false,
            &|| {
                *loader_called.borrow_mut() = true;
                // Take the cache out of the RefCell
                Ok(cache.borrow_mut().take())
            },
            &|_| {
                *saver_count.borrow_mut() += 1;
                Ok(())
            },
            &|_| {
                *scanner_called.borrow_mut() = true;
                panic!("should not do full scan when cache is valid")
            },
        );

        assert!(result.is_ok());
        assert!(loader_called.into_inner());
        // Note: When the directory in dir_mtimes doesn't exist, validate_and_update
        // treats it as "changed" and removes projects under that path.
        // This test verifies the flow completes - the specific behavior of
        // validate_and_update is tested separately in cache.rs
        let result_projects = result.unwrap();
        // Projects were removed because the tracked directory doesn't exist
        assert!(result_projects.is_empty());
    }

    #[test]
    fn should_update_incrementally_when_cache_changed() {
        let config = create_test_config(true);
        let initial_projects = vec![PathBuf::from("/nonexistent/project1")];
        // Use a path that doesn't exist - validate_and_update will treat missing
        // directory as a change (unwrap_or(true) in the mtime check)
        let mut dir_mtimes = HashMap::new();
        dir_mtimes.insert(PathBuf::from("/definitely_nonexistent_path_abc"), 0u64);

        // Use RefCell<Option<>> to allow moving into closure multiple times
        let cache = RefCell::new(Some(ProjectCache::new(initial_projects, dir_mtimes)));

        let loader_called = RefCell::new(false);
        let saver_called = RefCell::new(false);

        let result = get_projects_internal(
            &config,
            false,
            &|| {
                *loader_called.borrow_mut() = true;
                // Take the cache out of the RefCell
                Ok(cache.borrow_mut().take())
            },
            &|_| {
                *saver_called.borrow_mut() = true;
                Ok(())
            },
            &|_| panic!("full scan should not happen with incremental update"),
        );

        // validate_and_update is called internally. Since the directory doesn't exist,
        // it treats it as "changed" and will try to rescan using scan_from_root.
        // We verify the flow completes without panicking.

        assert!(result.is_ok());
        assert!(loader_called.into_inner());
        // Note: The saver may or may not be called depending on whether
        // validate_and_update detects changes (missing dir = change)
    }
}
