use crate::common::home_dir;
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, target_path: &Path) -> Result<config::Repo> {
    if config.repos.is_empty() {
        return Err(anyhow::anyhow!("No repositories configured"));
    }

    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 {
                let repo_root = config.repos_path().join(&r.name);
                match crate::dot::insignore::match_repo_target_path(&repo_root, target_path) {
                    Ok(Some(ignore_file)) => {
                        println!(
                            "{}",
                            crate::dot::insignore::format_repo_skip_message(
                                &r.name,
                                target_path,
                                &ignore_file
                            )
                        );
                        false
                    }
                    Ok(None) => true,
                    Err(err) => {
                        eprintln!(
                            "{} Failed to evaluate .insignore for repository '{}': {}",
                            char::from(NerdFont::Warning).to_string().yellow(),
                            r.name,
                            err
                        );
                        false
                    }
                }
            }
        })
        .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 available for this path"
        ));
    }

    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, target_path: &Path) -> Result<DotfileDir> {
    let home = crate::dot::sources::home_dir();
    let is_root_target = !target_path.starts_with(&home);
    let dirs: Vec<_> = dotfile_repo
        .dotfile_dirs
        .iter()
        .filter(|d| d.is_root == is_root_target)
        .cloned()
        .collect();

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

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

    let items: Vec<DotsDirSelectItem> = dirs
        .into_iter()
        .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,
    force: bool,
    encrypt: bool,
    include_root: bool,
    debug: bool,
) -> Result<()> {
    let all_dotfiles = get_all_dotfiles(config, db, include_root)?;
    let target_path = resolve_dotfile_path(path, include_root, true)?;
    let home = home_dir();

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

    // Handle --choose flag for single files
    if choose && target_path.is_file() {
        if tracked_dotfiles.is_empty()
            && !force
            && let Some(ignore_file) = crate::dot::insignore::match_home_path(&target_path)?
        {
            println!(
                "{}",
                crate::dot::insignore::format_skip_message(&target_path, &ignore_file)
            );
            return Ok(());
        }

        return add_with_destination_picker(config, db, &target_path, force);
    }

    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, force)?;
        add_untracked_files(
            &untracked_files,
            config,
            db,
            &mut stats,
            force,
            encrypt,
            debug,
        )?;
    } else if target_path.is_file() && tracked_dotfiles.is_empty() {
        if !force && let Some(ignore_file) = crate::dot::insignore::match_home_path(&target_path)? {
            println!(
                "{}",
                crate::dot::insignore::format_skip_message(&target_path, &ignore_file)
            );
            return Ok(());
        }

        // Single untracked file - prompt to add it
        if let Some(repo_path) = add_new_file(config, db, &target_path, force, encrypt)? {
            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,
    force: bool,
) -> Result<()> {
    super::alternative::pick_destination_and_add(config, target_path, force)?;
    Ok(())
}

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

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

    if !force && is_path_ignored_by_repo(config, &repo_config.name, full_path)? {
        return Ok(None);
    }

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

    // 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(),
    };

    let recipients = if encrypt {
        let parsed = crate::dot::encryption::parse_recipients(&dotfile_repo.meta.encryption_recipients)
            .with_context(|| {
                format!(
                    "repository '{}' has no usable encryption_recipients configured in instantdots.toml.\n\
                     Please authorize decryption keys first using 'ins dot keys authorize'.",
                    repo_config.name
                )
            })?;
        Some(parsed)
    } else {
        None
    };

    // Use shared add function
    if add_to_destination(config, db, full_path, &dest, force, recipients.as_deref())? {
        Ok(Some(repo_base))
    } else {
        Ok(None)
    }
}

fn is_path_ignored_by_repo(
    config: &DotfileConfig,
    repo_name: &str,
    target_path: &Path,
) -> Result<bool> {
    let repo_root = config.repos_path().join(repo_name);

    if let Some(ignore_file) =
        crate::dot::insignore::match_repo_target_path(&repo_root, target_path)?
    {
        println!(
            "{}",
            crate::dot::insignore::format_skip_message(target_path, &ignore_file)
        );
        return Ok(true);
    }

    Ok(false)
}

/// Scan directory and categorize files as tracked or untracked
fn scan_and_categorize_files(
    dir_path: &Path,
    all_dotfiles: &HashMap<PathBuf, Dotfile>,
    force: bool,
) -> Result<(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 if !force
                && let Some(ignore_file) = crate::dot::insignore::match_home_path(file_path)?
            {
                println!(
                    "{}",
                    crate::dot::insignore::format_skip_message(file_path, &ignore_file)
                );
            } else {
                untracked_files.push(file_path.to_path_buf());
            }
        }
    }

    Ok((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, config)?;

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

    let home = home_dir();
    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,
    force: bool,
    encrypt: bool,
    _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 {
        if let Some(repo_path) = add_new_file(config, db, file_path, force, encrypt)? {
            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,
        );
    }
}
