use crate::dot::config::{self, DotfileConfig};
use crate::dot::db::Database;
use crate::dot::dotfile::Dotfile;
use crate::dot::dotfilerepo::{DotfileDir, DotfileRepo};
use crate::dot::menu::repo_actions::build_repo_preview;
use crate::dot::types::{DotsDirSelectItem, RepoMenuItem};
use crate::dot::utils::{filter_dotfiles_by_path, get_all_dotfiles, resolve_dotfile_path};
use crate::menu_utils::{FzfResult, FzfWrapper, Header};
use crate::ui::catppuccin::fzf_mocha_args;
use crate::ui::prelude::*;
use anyhow::Result;
use colored::*;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

/// Statistics for directory add operation
pub struct DirectoryAddStats {
    pub updated_count: usize,
    pub unchanged_count: usize,
    pub added_count: usize,
    pub modified_repos: HashSet<PathBuf>,
}

impl DirectoryAddStats {
    pub fn new() -> Self {
        Self {
            updated_count: 0,
            unchanged_count: 0,
            added_count: 0,
            modified_repos: HashSet::new(),
        }
    }

    pub fn has_changes(&self) -> bool {
        self.updated_count > 0 || self.unchanged_count > 0 || self.added_count > 0
    }
}

/// Prompt the user to select one of the configured repositories
fn select_repo(config: &DotfileConfig, db: &Database) -> Result<config::Repo> {
    if config.repos.is_empty() {
        return Err(anyhow::anyhow!("No repositories configured"));
    }

    if config.repos.len() == 1 {
        let repo = &config.repos[0];
        if !repo.read_only {
            return Ok(repo.clone());
        }
        // If the only repo is read-only, fall through to filtering logic which will return error
    }

    let items: Vec<RepoMenuItem> = config
        .repos
        .iter()
        .filter(|r| {
            if r.read_only {
                println!(
                    "{} Skipping read-only repository '{}'",
                    char::from(NerdFont::ArrowRight).to_string().blue(),
                    r.name
                );
                false
            } else {
                true
            }
        })
        .map(|repo| RepoMenuItem {
            preview: build_repo_preview(&repo.name, config, db),
            repo: repo.clone(),
        })
        .collect();

    if items.is_empty() {
        return Err(anyhow::anyhow!("No writable repositories configured"));
    }

    match FzfWrapper::builder()
        .header(Header::fancy("Select Repository"))
        .prompt("Select")
        .args(fzf_mocha_args())
        .responsive_layout()
        .select(items)
        .map_err(|e| anyhow::anyhow!("Selection error: {}", e))?
    {
        FzfResult::Selected(item) => Ok(item.repo),
        FzfResult::Cancelled => Err(anyhow::anyhow!("No repository selected")),
        FzfResult::Error(e) => Err(anyhow::anyhow!("Selection error: {}", e)),
        _ => Err(anyhow::anyhow!("Unexpected selection result")),
    }
}

/// Prompt the user to select one of the repo's configured `dots_dirs`
fn select_dots_dir(dotfile_repo: &DotfileRepo) -> Result<DotfileDir> {
    let dirs = &dotfile_repo.dotfile_dirs;

    if dirs.is_empty() {
        return Err(anyhow::anyhow!(
            "Repository '{}' has no configured dots_dirs",
            dotfile_repo.name
        ));
    }

    if dirs.len() == 1 {
        return Ok(dirs[0].clone());
    }

    let items: Vec<DotsDirSelectItem> = dirs
        .iter()
        .cloned()
        .map(|dots_dir| DotsDirSelectItem {
            dots_dir,
            repo_name: dotfile_repo.name.clone(),
        })
        .collect();

    match FzfWrapper::builder()
        .header(Header::fancy(&format!(
            "Select Directory in '{}'",
            dotfile_repo.name
        )))
        .prompt("Select")
        .args(fzf_mocha_args())
        .responsive_layout()
        .select(items)
        .map_err(|e| anyhow::anyhow!("Selection error: {}", e))?
    {
        FzfResult::Selected(item) => Ok(item.dots_dir),
        FzfResult::Cancelled => Err(anyhow::anyhow!("No dots directory selected")),
        FzfResult::Error(e) => Err(anyhow::anyhow!("Selection error: {}", e)),
        _ => Err(anyhow::anyhow!("Unexpected selection result")),
    }
}

/// Add dotfiles to tracking or update existing tracked files
///
/// Behavior:
/// - For tracked files: Update the source file (fetch behavior)
/// - For untracked files: Prompt to add them to a repository
/// - For directories without --all: Only update tracked files
/// - For directories with --all: Update tracked files AND add untracked files
/// - With --choose: Pick which repo/subdir to add the file to
pub fn add_dotfile(
    config: &DotfileConfig,
    db: &Database,
    path: &str,
    add_all: bool,
    choose: bool,
    debug: bool,
) -> Result<()> {
    let all_dotfiles = get_all_dotfiles(config, db)?;
    let target_path = resolve_dotfile_path(path)?;
    let home = PathBuf::from(shellexpand::tilde("~").to_string());

    // Handle --choose flag for single files
    if choose && target_path.is_file() {
        return add_with_destination_picker(config, db, &target_path);
    }

    // Get tracked dotfiles within the specified path
    let tracked_dotfiles = filter_dotfiles_by_path(&all_dotfiles, &target_path);

    let mut stats = DirectoryAddStats::new();

    // Update tracked files
    if !tracked_dotfiles.is_empty() {
        update_tracked_dotfiles(&tracked_dotfiles, config, db, &mut stats, debug)?;
    }

    // Handle untracked files
    if add_all {
        // Scan for untracked files and add them
        let (_, untracked_files) = scan_and_categorize_files(&target_path, &all_dotfiles);
        add_untracked_files(&untracked_files, config, db, &mut stats, debug)?;
    } else if target_path.is_file() && tracked_dotfiles.is_empty() {
        // Single untracked file - prompt to add it
        let repo_path = add_new_file(config, db, &target_path)?;
        stats.added_count += 1;
        stats.modified_repos.insert(repo_path);
    } else if tracked_dotfiles.is_empty() {
        // Directory with no tracked files
        let relative_dir = target_path.strip_prefix(&home).unwrap_or(&target_path);
        emit(
            Level::Info,
            "dot.add.no_tracked",
            &format!(
                "{} No tracked files found in ~/{}\n  Use 'ins dot add --all ~/{} to add untracked files",
                char::from(NerdFont::Info),
                relative_dir.display(),
                relative_dir.display()
            ),
            None,
        );
        return Ok(());
    }

    db.cleanup_hashes(config.hash_cleanup_days)?;
    print_directory_add_summary(&stats);

    Ok(())
}

/// Add a file with a destination picker (for --choose flag).
/// Delegates to the shared picker in the alternative module.
fn add_with_destination_picker(
    config: &DotfileConfig,
    _db: &Database,
    target_path: &Path,
) -> Result<()> {
    super::alternative::pick_destination_and_add(config, target_path)?;
    Ok(())
}

/// Add a new untracked file and return the repo path
fn add_new_file(config: &DotfileConfig, db: &Database, full_path: &Path) -> Result<PathBuf> {
    use super::alternative::add_to_destination;
    use crate::dot::override_config::DotfileSource;

    // Repository selection
    let repo_config = select_repo(config, db)?;
    let dotfile_repo = DotfileRepo::new(config, repo_config.name.clone())?;

    // dots_dir selection
    let chosen_dir = select_dots_dir(&dotfile_repo)?;

    // Build destination info
    let repo_base = dotfile_repo.local_path(config)?;
    let dest = DotfileSource {
        repo_name: repo_config.name.clone(),
        subdir_name: chosen_dir
            .path
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_else(|| ".".to_string()),
        source_path: chosen_dir.path.clone(),
    };

    // Use shared add function
    add_to_destination(config, db, full_path, &dest)?;

    Ok(repo_base)
}

/// Scan directory and categorize files as tracked or untracked
fn scan_and_categorize_files(
    dir_path: &Path,
    all_dotfiles: &HashMap<PathBuf, Dotfile>,
) -> (Vec<PathBuf>, Vec<PathBuf>) {
    let mut tracked_files = Vec::new();
    let mut untracked_files = Vec::new();

    for entry in WalkDir::new(dir_path)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|entry| {
            let path_str = entry.path().to_string_lossy();
            !path_str.contains("/.git/")
        })
    {
        if entry.file_type().is_file() {
            let file_path = entry.path();

            if all_dotfiles.contains_key(file_path) {
                tracked_files.push(file_path.to_path_buf());
            } else {
                untracked_files.push(file_path.to_path_buf());
            }
        }
    }

    (tracked_files, untracked_files)
}

/// Update a single tracked dotfile and return whether it was updated or unchanged
fn update_single_dotfile(dotfile: &Dotfile, config: &DotfileConfig, db: &Database) -> Result<bool> {
    let old_source_hash = if dotfile.source_path.exists() {
        Some(Dotfile::compute_hash(&dotfile.source_path)?)
    } else {
        None
    };

    dotfile.fetch(db)?;

    let new_source_hash = Dotfile::compute_hash(&dotfile.source_path)?;
    let has_changes = old_source_hash.as_ref() != Some(&new_source_hash);

    let home = PathBuf::from(shellexpand::tilde("~").to_string());
    let relative_path = dotfile
        .target_path
        .strip_prefix(&home)
        .unwrap_or(&dotfile.target_path);

    if has_changes {
        println!(
            "{} Updated ~/{} (changes detected)",
            char::from(NerdFont::Check),
            relative_path.display().to_string().green()
        );
    } else {
        let repo_name = crate::dot::git::get_repo_name_for_dotfile(dotfile, config);
        let subdir_name = crate::dot::git::get_dotfile_dir_name(dotfile, config);
        println!(
            "{} ~/{} is already up to date in {}/{}",
            char::from(NerdFont::Check).to_string().dimmed(),
            relative_path.display().to_string().dimmed(),
            repo_name.as_str().dimmed(),
            subdir_name.dimmed()
        );
    }

    Ok(has_changes)
}

/// Update multiple tracked dotfiles
fn update_tracked_dotfiles(
    dotfiles: &[&Dotfile],
    config: &DotfileConfig,
    db: &Database,
    stats: &mut DirectoryAddStats,
    debug: bool,
) -> Result<()> {
    for dotfile in dotfiles {
        // Check if repo is read-only
        let repo_name = crate::dot::git::get_repo_name_for_dotfile(dotfile, config);
        if let Some(repo) = config.repos.iter().find(|r| r.name == repo_name.as_str())
            && repo.read_only
        {
            println!(
                "{} Skipping update for read-only repository '{}'",
                char::from(NerdFont::ArrowRight).to_string().blue(),
                repo.name
            );
            continue;
        }

        let was_updated = update_single_dotfile(dotfile, config, db)?;
        if was_updated {
            stats.updated_count += 1;
            let dotfile_repo = DotfileRepo::new(config, repo_name.to_string())?;
            let repo_path = dotfile_repo.local_path(config)?;

            // Automatically stage the updated file
            // resolve_dotfile_path might be needed if source_path is not absolute, but Dotfile struct seems to keep absolute paths.
            // checking dotfile.rs... source_path should be absolute.
            if let Err(e) =
                crate::dot::git::repo_ops::git_add(&repo_path, &dotfile.source_path, debug)
            {
                eprintln!(
                    "{} Failed to stage file: {}",
                    char::from(NerdFont::Warning).to_string().yellow(),
                    e
                );
            }

            stats.modified_repos.insert(repo_path);
        } else {
            stats.unchanged_count += 1;
        }
    }
    Ok(())
}

/// Add multiple untracked files
fn add_untracked_files(
    file_paths: &[PathBuf],
    config: &DotfileConfig,
    db: &Database,
    stats: &mut DirectoryAddStats,
    _debug: bool,
) -> Result<()> {
    if file_paths.is_empty() {
        return Ok(());
    }

    println!(
        "\n{} Adding {} untracked file(s)...",
        char::from(NerdFont::Info),
        file_paths.len()
    );

    for file_path in file_paths {
        let repo_path = add_new_file(config, db, file_path)?;
        stats.added_count += 1;
        stats.modified_repos.insert(repo_path);
    }

    Ok(())
}

/// Print summary of directory add operation
fn print_directory_add_summary(stats: &DirectoryAddStats) {
    if stats.has_changes() {
        emit(
            Level::Success,
            "dot.add.complete",
            &format!(
                "{} Complete: {} updated, {} unchanged{}",
                char::from(NerdFont::Check),
                stats.updated_count,
                stats.unchanged_count,
                if stats.added_count > 0 {
                    format!(", {} added", stats.added_count)
                } else {
                    String::new()
                }
            ),
            None,
        );

        if !stats.modified_repos.is_empty() {
            let mut repos: Vec<_> = stats.modified_repos.iter().collect();
            repos.sort();

            println!(
                "{} Modified repositories:",
                char::from(NerdFont::Info).to_string().blue()
            );
            for repo_path in repos {
                println!("  {}", repo_path.display().to_string().cyan());
            }
        }
    } else {
        emit(
            Level::Info,
            "dot.add.no_changes",
            &format!("{} No changes to process", char::from(NerdFont::Info)),
            None,
        );
    }
}
