use crate::tool::command::RmUselessDirsArgs;
use crate::tool::config::{Config, DirListConfig, ListMode};
use anyhow::{Result, anyhow};
use ignore::Match;
use ignore::gitignore::Gitignore;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

pub fn run(args: &RmUselessDirsArgs) -> Result<()> {
    let target = match &args.path {
        Some(path) => path.clone(),
        None => std::env::current_dir()?,
    };

    if !target.exists() {
        return Err(anyhow!(t!(
            "tool.rm_empty_dirs.error.target_not_exist",
            path = target.display()
        )));
    }

    if !target.is_dir() {
        return Err(anyhow!(
            "{}",
            t!(
                "tool.rm_empty_dirs.error.target_not_dir",
                path = target.display()
            )
        ));
    }

    let config = Config::load_with_priority_or_default(args.config.as_deref())?;
    let rm_config = config.get_rm_useless_dirs_config();
    let protected_dirs = resolve_dir_names(
        DEFAULT_PROTECTED_DIRS,
        rm_config.and_then(|cfg| cfg.protected.as_ref()),
    );
    let useless_dirs = resolve_dir_names(
        DEFAULT_USELESS_DIRS,
        rm_config.and_then(|cfg| cfg.useless.as_ref()),
    );

    let dry_run = !args.yes;
    let gitignore_path = resolve_gitignore_path(&target, args)?;
    let gitignore = match gitignore_path.as_ref() {
        Some(path) => Some(load_gitignore(path)?),
        None => None,
    };

    if is_protected_target(&target, &protected_dirs) {
        eprintln!(
            "{}",
            t!(
                "tool.rm_empty_dirs.skipping_protected_path",
                path = target.display()
            )
        );
        return Ok(());
    }

    println!("{}", t!("tool.rm_empty_dirs.start_title"));
    println!(
        "{}",
        t!("tool.rm_empty_dirs.target_label", path = target.display())
    );
    if dry_run {
        println!("{}", t!("tool.rm_empty_dirs.dry_run_enabled"));
    } else {
        println!("{}", t!("tool.rm_empty_dirs.live_mode_enabled"));
    }
    if args.prune_ignored {
        println!("{}", t!("tool.rm_empty_dirs.prune_ignored_enabled"));
    }
    if args.verbose {
        if let Some(path) = gitignore_path.as_ref() {
            println!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.verbose_gitignore_using",
                    path = path.display()
                )
            );
        } else {
            println!("{}", t!("tool.rm_empty_dirs.verbose_gitignore_none"));
        }
    }

    let mut report = RemovalReport::default();
    let options = Options {
        dry_run,
        gitignore,
        prune_ignored: args.prune_ignored,
        verbose: args.verbose,
        protected_dirs,
        useless_dirs,
    };

    let root_empty = process_dir(&target, false, &options, &mut report)?;

    if root_empty {
        if dry_run {
            println!("{}", t!("tool.rm_empty_dirs.note_root_would_be_empty"));
        } else {
            println!("{}", t!("tool.rm_empty_dirs.note_root_empty"));
        }
    }

    println!("\n{}", t!("tool.rm_empty_dirs.summary_title"));
    println!(
        "{}",
        t!(
            "tool.rm_empty_dirs.summary_empty_dirs_found",
            count = report.useless_dirs_found
        )
    );
    if !dry_run {
        println!(
            "{}",
            t!(
                "tool.rm_empty_dirs.summary_empty_dirs_removed",
                count = report.targets.len()
            )
        );
        if !report.failed.is_empty() {
            println!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.summary_empty_dirs_failed",
                    count = report.failed.len()
                )
            );
        }
    }
    println!(
        "{}",
        t!(
            "tool.rm_empty_dirs.summary_dirs_scanned",
            count = report.dirs_scanned
        )
    );
    println!(
        "{}",
        t!(
            "tool.rm_empty_dirs.summary_files_scanned",
            count = report.files_scanned
        )
    );
    let ignored_entries = report.ignored_dirs + report.ignored_files;
    if ignored_entries == 0 {
        println!("{}", t!("tool.rm_empty_dirs.summary_ignored_entries_zero"));
    } else {
        println!(
            "{}",
            t!(
                "tool.rm_empty_dirs.summary_ignored_entries",
                total = ignored_entries,
                dirs = report.ignored_dirs,
                files = report.ignored_files
            )
        );
    }
    println!(
        "{}",
        t!("tool.rm_empty_dirs.summary_errors", count = report.errors)
    );

    if !report.targets.is_empty() {
        let label = if dry_run {
            t!("tool.rm_empty_dirs.list_to_remove")
        } else {
            t!("tool.rm_empty_dirs.list_removed")
        };
        println!("\n{}:", label);
        for dir in &report.targets {
            println!("  - {}", dir.display());
        }
    }

    if !report.failed.is_empty() {
        println!("\n{}:", t!("tool.rm_empty_dirs.list_failed_title"));
        for dir in &report.failed {
            println!("  - {}", dir.display());
        }
    }

    Ok(())
}

#[derive(Default)]
struct RemovalReport {
    targets: Vec<PathBuf>,
    failed: Vec<PathBuf>,
    errors: usize,
    useless_dirs_found: usize,
    dirs_scanned: usize,
    files_scanned: usize,
    ignored_dirs: usize,
    ignored_files: usize,
}

struct Options {
    dry_run: bool,
    gitignore: Option<Gitignore>,
    prune_ignored: bool,
    verbose: bool,
    protected_dirs: HashSet<String>,
    useless_dirs: HashSet<String>,
}

const DEFAULT_PROTECTED_DIRS: &[&str] = &[
    ".git",
    ".hg",
    ".svn",
    ".bzr",
    ".idea",
    ".vscode",
    "node_modules",
    ".yarn",
    ".pnpm-store",
    ".pnpm",
    ".npm",
    ".cargo",
    ".venv",
    "venv",
    ".tox",
    ".nox",
    "__pypackages__",
    "target",
    "vendor",
];

const DEFAULT_USELESS_DIRS: &[&str] = &[
    "__pycache__",
    ".pytest_cache",
    ".mypy_cache",
    ".ruff_cache",
    ".basedpyright",
    ".pytype",
    ".pyre",
    ".ty",
    ".ty_cache",
    ".ty-cache",
];

fn resolve_dir_names(defaults: &[&str], config: Option<&DirListConfig>) -> HashSet<String> {
    let mode = config.map(|cfg| cfg.mode).unwrap_or(ListMode::Extend);
    let mut names = HashSet::new();
    match mode {
        ListMode::Extend => {
            for name in defaults {
                names.insert((*name).to_string());
            }
            if let Some(cfg) = config {
                for name in &cfg.names {
                    names.insert(name.clone());
                }
            }
        }
        ListMode::Override => {
            if let Some(cfg) = config {
                for name in &cfg.names {
                    names.insert(name.clone());
                }
            }
        }
    }
    names
}

fn is_protected_target(target: &Path, protected_dirs: &HashSet<String>) -> bool {
    target.iter().any(|component| {
        component
            .to_str()
            .is_some_and(|name| protected_dirs.contains(name))
    })
}

fn process_dir(
    dir: &Path,
    parent_ignored: bool,
    options: &Options,
    report: &mut RemovalReport,
) -> Result<bool> {
    report.dirs_scanned += 1;
    let entries = match fs::read_dir(dir) {
        Ok(entries) => entries,
        Err(err) => {
            report.errors += 1;
            eprintln!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.read_dir_failed",
                    path = dir.display(),
                    error = err
                )
            );
            return Ok(false);
        }
    };

    let mut is_empty = true;
    let mut ignored_files = Vec::new();

    for entry in entries {
        let entry = match entry {
            Ok(entry) => entry,
            Err(err) => {
                report.errors += 1;
                eprintln!(
                    "{}",
                    t!(
                        "tool.rm_empty_dirs.error.read_entry_failed",
                        path = dir.display(),
                        error = err
                    )
                );
                is_empty = false;
                continue;
            }
        };

        let path = entry.path();
        let file_type = match entry.file_type() {
            Ok(file_type) => file_type,
            Err(err) => {
                report.errors += 1;
                eprintln!(
                    "{}",
                    t!(
                        "tool.rm_empty_dirs.error.read_file_type_failed",
                        path = path.display(),
                        error = err
                    )
                );
                is_empty = false;
                continue;
            }
        };

        let is_dir = file_type.is_dir();
        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        if is_dir && options.protected_dirs.contains(name_str.as_ref()) {
            if options.verbose {
                println!(
                    "{}",
                    t!(
                        "tool.rm_empty_dirs.skipping_protected_path",
                        path = path.display()
                    )
                );
            }
            is_empty = false;
            continue;
        }

        if is_dir && options.useless_dirs.contains(name_str.as_ref()) {
            report.useless_dirs_found += 1;
            if try_remove_useless_dir(&path, options, report) {
                report.targets.push(path.clone());
            } else {
                is_empty = false;
            }
            continue;
        }

        let match_result = match options.gitignore.as_ref() {
            Some(gitignore) => gitignore.matched(&path, is_dir),
            None => Match::None,
        };
        let ignored_by_match = matches!(match_result, Match::Ignore(_));
        let is_whitelisted = matches!(match_result, Match::Whitelist(_));
        let ignored = if parent_ignored {
            !is_whitelisted
        } else {
            ignored_by_match
        };
        if ignored {
            if is_dir {
                report.ignored_dirs += 1;
            } else {
                report.ignored_files += 1;
            }
            if !options.prune_ignored {
                if options.verbose {
                    println!(
                        "{}",
                        t!(
                            "tool.rm_empty_dirs.skipping_ignored_path",
                            path = path.display()
                        )
                    );
                }
                is_empty = false;
                continue;
            }

            if !is_dir {
                ignored_files.push(path);
                continue;
            }
        }

        if is_dir {
            let child_parent_ignored = if is_whitelisted {
                false
            } else {
                parent_ignored || ignored_by_match
            };
            let child_empty = process_dir(&path, child_parent_ignored, options, report)?;
            if child_empty {
                report.useless_dirs_found += 1;
                if try_remove_dir(&path, options, report) {
                    report.targets.push(path.clone());
                } else {
                    is_empty = false;
                }
            } else {
                is_empty = false;
            }
        } else {
            report.files_scanned += 1;
            is_empty = false;
        }
    }

    if options.prune_ignored && is_empty && !ignored_files.is_empty() {
        for file in ignored_files {
            if !try_remove_ignored_file(&file, options, report) {
                is_empty = false;
            }
        }
    }

    Ok(is_empty)
}

fn try_remove_dir(path: &Path, options: &Options, report: &mut RemovalReport) -> bool {
    if options.verbose {
        if options.dry_run {
            println!(
                "{}",
                t!("tool.rm_empty_dirs.would_remove_dir", path = path.display())
            );
        } else {
            println!(
                "{}",
                t!("tool.rm_empty_dirs.removing_dir", path = path.display())
            );
        }
    }

    if options.dry_run {
        return true;
    }

    match fs::remove_dir(path) {
        Ok(()) => true,
        Err(err) => {
            report.errors += 1;
            report.failed.push(path.to_path_buf());
            eprintln!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.remove_dir_failed",
                    path = path.display(),
                    error = err
                )
            );
            false
        }
    }
}

fn try_remove_useless_dir(path: &Path, options: &Options, report: &mut RemovalReport) -> bool {
    if contains_protected_dir(path, options, report) {
        return false;
    }

    if options.verbose {
        if options.dry_run {
            println!(
                "{}",
                t!("tool.rm_empty_dirs.would_remove_dir", path = path.display())
            );
        } else {
            println!(
                "{}",
                t!("tool.rm_empty_dirs.removing_dir", path = path.display())
            );
        }
    }

    if options.dry_run {
        return true;
    }

    match fs::remove_dir_all(path) {
        Ok(()) => true,
        Err(err) => {
            report.errors += 1;
            report.failed.push(path.to_path_buf());
            eprintln!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.remove_dir_failed",
                    path = path.display(),
                    error = err
                )
            );
            false
        }
    }
}

fn contains_protected_dir(path: &Path, options: &Options, report: &mut RemovalReport) -> bool {
    if options.protected_dirs.is_empty() {
        return false;
    }

    let entries = match fs::read_dir(path) {
        Ok(entries) => entries,
        Err(err) => {
            report.errors += 1;
            eprintln!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.read_dir_failed",
                    path = path.display(),
                    error = err
                )
            );
            return true;
        }
    };

    for entry in entries {
        let entry = match entry {
            Ok(entry) => entry,
            Err(err) => {
                report.errors += 1;
                eprintln!(
                    "{}",
                    t!(
                        "tool.rm_empty_dirs.error.read_entry_failed",
                        path = path.display(),
                        error = err
                    )
                );
                return true;
            }
        };

        let file_type = match entry.file_type() {
            Ok(file_type) => file_type,
            Err(err) => {
                report.errors += 1;
                eprintln!(
                    "{}",
                    t!(
                        "tool.rm_empty_dirs.error.read_file_type_failed",
                        path = entry.path().display(),
                        error = err
                    )
                );
                return true;
            }
        };

        if !file_type.is_dir() {
            continue;
        }

        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        if options.protected_dirs.contains(name_str.as_ref()) {
            return true;
        }

        if contains_protected_dir(&entry.path(), options, report) {
            return true;
        }
    }

    false
}

fn try_remove_ignored_file(path: &Path, options: &Options, report: &mut RemovalReport) -> bool {
    if options.verbose {
        if options.dry_run {
            println!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.would_remove_ignored_file",
                    path = path.display()
                )
            );
        } else {
            println!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.removing_ignored_file",
                    path = path.display()
                )
            );
        }
    }

    if options.dry_run {
        return true;
    }

    match fs::remove_file(path) {
        Ok(()) => true,
        Err(err) => {
            report.errors += 1;
            eprintln!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.remove_ignored_file_failed",
                    path = path.display(),
                    error = err
                )
            );
            false
        }
    }
}

fn resolve_gitignore_path(target: &Path, args: &RmUselessDirsArgs) -> Result<Option<PathBuf>> {
    if let Some(path) = &args.gitignore {
        if !path.exists() {
            return Err(anyhow!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.gitignore_not_exist",
                    path = path.display()
                )
            ));
        }
        if !path.is_file() {
            return Err(anyhow!(
                "{}",
                t!(
                    "tool.rm_empty_dirs.error.gitignore_not_file",
                    path = path.display()
                )
            ));
        }
        return Ok(Some(path.clone()));
    }

    if let Some(repo_root) = find_git_repo_root(target) {
        let repo_gitignore = repo_root.join(".gitignore");
        if repo_gitignore.is_file() {
            return Ok(Some(repo_gitignore));
        }
    }

    let default_path = target.join(".gitignore");
    Ok(default_path.is_file().then_some(default_path))
}

fn load_gitignore(path: &Path) -> Result<Gitignore> {
    let (gitignore, err) = Gitignore::new(path);
    if let Some(err) = err {
        eprintln!(
            "{}",
            t!(
                "tool.rm_empty_dirs.error.gitignore_parse_warning",
                path = path.display(),
                error = err
            )
        );
    }
    Ok(gitignore)
}

fn find_git_repo_root(target: &Path) -> Option<PathBuf> {
    target
        .ancestors()
        .find(|candidate| candidate.join(".git").exists())
        .map(Path::to_path_buf)
}
