use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::process::exit;
use std::time::{Duration, Instant};

use color_eyre::eyre::{self, WrapErr};
use color_eyre::Result;
use indicatif::{ProgressBar, ProgressStyle};
#[cfg(unix)]
use nix::unistd;

use crate::dirs::*;
use crate::models::*;
#[cfg(unix)]
use crate::utils;
use crate::lfs;

#[derive(Debug)]
pub struct DeinitPlan {
    pub dirs_to_remove: Vec<PathBuf>,
    pub shell_rc_files: Vec<String>,
    pub symlinks_to_remove: Vec<PathBuf>,
    /// User-created redirect symlinks (e.g. ~/.epkg -> /Volumes/epkg).
    /// These are preserved; only contents inside the target are removed.
    pub redirect_symlinks: Vec<PathBuf>,
}

impl DeinitPlan {
    pub fn new() -> Self {
        Self {
            dirs_to_remove: Vec::new(),
            shell_rc_files: Vec::new(),
            symlinks_to_remove: Vec::new(),
            redirect_symlinks: Vec::new(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.dirs_to_remove.is_empty() &&
        self.shell_rc_files.is_empty() &&
        self.symlinks_to_remove.is_empty() &&
        self.redirect_symlinks.is_empty()
    }
}

/// Check if a path is a user-created redirect symlink or mountpoint.
/// A redirect is when user intentionally redirected storage to external volume:
/// - Symlink pointing outside standard epkg locations (home, /opt/epkg)
/// - Mountpoint (bind mount or real mount) at epkg standard location
///
/// Returns the detected redirect type and target info if applicable.
#[derive(Debug, Clone, PartialEq)]
pub enum RedirectType {
    /// No redirect - standard directory
    None,
    /// Symlink to external location (target path stored)
    Symlink(PathBuf),
    /// Mountpoint at this location (device ID stored for verification)
    /// Note: Only constructed on Unix; Windows doesn't have mountpoints in this sense.
    #[allow(dead_code)]
    Mountpoint,
}

/// Check if a symlink target is outside standard epkg locations.
fn is_external_redirect_target(target: &Path, home: &str) -> bool {
    let target_str = target.to_string_lossy();
    // Check if target is outside standard epkg locations:
    // - Not under home directory
    // - Not under /opt/epkg (or Windows epkg roots)
    #[cfg(not(windows))]
    {
        !target_str.starts_with(home) && !target_str.starts_with("/opt/epkg")
    }
    #[cfg(windows)]
    {
        !target_str.starts_with(home) &&
        !target_str.starts_with("C:\\epkg") &&
        !target_str.starts_with("D:\\epkg")
    }
}

/// Check if a path is a user-created redirect (symlink or mountpoint).
fn check_redirect(path: &Path, home: &str) -> RedirectType {
    // First check for symlink
    if lfs::is_symlink(path) {
        if let Ok(target) = fs::read_link(path) {
            if is_external_redirect_target(&target, home) {
                return RedirectType::Symlink(target);
            }
        }
    }

    // Check for mountpoint (Unix only)
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        if let Ok(meta) = lfs::symlink_metadata(path) {
            if meta.is_dir() {
                // Compare device ID with parent directory
                if let Some(parent) = path.parent() {
                    if let Ok(parent_meta) = lfs::symlink_metadata(parent) {
                        if meta.dev() != parent_meta.dev() {
                            // Different device = mountpoint
                            return RedirectType::Mountpoint;
                        }
                    }
                }
            }
        }
    }

    RedirectType::None
}

pub fn deinit_epkg(scope: &str) -> Result<()> {
    #[cfg(target_os = "linux")]
    crate::apparmor::remove_apparmor_profile()?;

    let plan = match scope {
        "personal" => collect_user_personal_plan()?,
        #[cfg(unix)]
        "global" => collect_global_deinit_plan()?,
        #[cfg(not(unix))]
        "global" => return Err(eyre::eyre!("Global deinitialization is not supported on this platform")),
        _ => return Err(eyre::eyre!("Invalid scope: {}. Must be 'personal' or 'global'", scope)),
    };

    execute_deinit_with_plan(plan, scope)
}

fn execute_deinit_with_plan(plan: DeinitPlan, scope: &str) -> Result<()> {
    if plan.is_empty() {
        return Ok(());
    }

    // Display plan and confirm
    display_deinit_plan(&plan, scope)?;
    if !confirm_deinit()? {
        println!("Deinitialization cancelled by user.");
        return Ok(());
    }

    // Execute the plan
    execute_deinit_plan(&plan)?;

    println!("Epkg deinitialization completed successfully.");
    println!("For changes to take effect, close and re-open your current shell.");
    Ok(())
}

/// Helper to add directory contents to removal plan when the parent is a redirect.
/// Appends subdirectory names to both the redirect path and its target.
fn add_redirect_contents_to_plan(
    plan: &mut DeinitPlan,
    target: &Path,
    subdirs: &[&str],
    symlinks: &[&str],
) {
    for subdir in subdirs {
        plan.dirs_to_remove.push(target.join(subdir));
    }
    for symlink in symlinks {
        plan.symlinks_to_remove.push(target.join(symlink));
    }
}

/// Helper to handle a path that might be a redirect (symlink or mountpoint).
/// Returns true if it's a redirect (handled), false if standard directory.
fn handle_redirect_path(
    plan: &mut DeinitPlan,
    path: &Path,
    home: &str,
    path_name: &str,
    subdirs: &[&str],
    symlinks: &[&str],
) -> bool {
    if !lfs::exists_no_follow(path) {
        return false;
    }

    match check_redirect(path, home) {
        RedirectType::Symlink(target) => {
            plan.redirect_symlinks.push(path.to_path_buf());
            println!("Note: {} is redirected to {} (symlink), will preserve redirect",
                     path_name, target.display());
            add_redirect_contents_to_plan(plan, &target, subdirs, symlinks);
            true
        }
        RedirectType::Mountpoint => {
            plan.redirect_symlinks.push(path.to_path_buf());
            println!("Note: {} is a mountpoint, will preserve mountpoint", path_name);
            // For mountpoint, use the path itself (it IS the directory)
            add_redirect_contents_to_plan(plan, path, subdirs, symlinks);
            true
        }
        RedirectType::None => false,
    }
}

#[cfg(unix)]
fn collect_global_deinit_plan() -> Result<DeinitPlan> {
    // We'll deinit every user! So check if running by root (effective UID)
    if !unistd::geteuid().is_root() {
        eprintln!("Global deinitialization requires root user.");
        exit(1);
    }

    let mut plan = DeinitPlan::new();
    let home_dir = get_home()?;
    let opt_epkg = dirs().opt_epkg.clone();

    if !lfs::exists_on_host(&opt_epkg) {
        println!("Global epkg directory {} does not exist.", opt_epkg.display());
        exit(1);
    }

    // Check if /opt/epkg is a redirect (symlink or mountpoint)
    if !handle_redirect_path(&mut plan, &opt_epkg, &home_dir, "/opt/epkg",
                             &["envs", "store", "cache"], &[]) {
        // Standard directory, remove it entirely
        plan.dirs_to_remove.push(opt_epkg);
    }

    // Remove /usr/local/bin/epkg symlink
    let usr_local_bin_epkg = PathBuf::from("/usr/local/bin/epkg");
    if lfs::exists_on_host(&usr_local_bin_epkg) {
        plan.symlinks_to_remove.push(usr_local_bin_epkg);
    }

    // Update global shell rc files - only add those that contain epkg content
    let global_shell_rcs = crate::dirs::get_global_shell_rc()?;
    for rc in global_shell_rcs {
        if rc_file_has_epkg(&rc) {
            plan.shell_rc_files.push(rc);
        }
    }

    Ok(plan)
}

fn collect_user_personal_plan() -> Result<DeinitPlan> {
    let mut plan = DeinitPlan::new();
    let home_dir = get_home()?;

    if config().init.shared_store {
        // Remove /opt/epkg/envs/$USER/
        let user_public_envs_path = dirs().user_envs.clone();
        if lfs::exists_on_host(&user_public_envs_path) {
            // Check if user_envs parent (/opt/epkg/envs) is a redirect
            let opt_epkg_envs = PathBuf::from("/opt/epkg/envs");
            if handle_redirect_path(&mut plan, &opt_epkg_envs, &home_dir, "/opt/epkg/envs",
                                    &[], &[]) {
                // Redirect detected - only remove user's subdirectory inside
            }
            // Remove user's envs directory contents
            plan.dirs_to_remove.push(user_public_envs_path);
        }

        // Remove /opt/epkg/cache/aur_builds/$USER/
        let user_aur_builds_path = dirs().user_aur_builds.clone();
        if lfs::exists_on_host(&user_aur_builds_path) {
            plan.dirs_to_remove.push(user_aur_builds_path);
        }
    } else {
        // Handle ~/.epkg/ - check for redirect symlink/mountpoint
        let home_epkg = dirs().home_epkg.clone();
        if !handle_redirect_path(&mut plan, &home_epkg, &home_dir, "~/.epkg",
                                 &["envs"], &["bin", "assets"]) {
            // Standard directory, remove it entirely
            if lfs::exists_on_host(&home_epkg) {
                plan.dirs_to_remove.push(home_epkg);
            }
        }

        // Handle cache directory - check for redirect symlink/mountpoint
        let home_cache = dirs().home_cache.clone();
        if !handle_redirect_path(&mut plan, &home_cache, &home_dir, "cache directory",
                                 &["channels"], &[]) {
            // Standard cache directory
            let channels_cache_dir = dirs().epkg_channels_cache.clone();
            if lfs::exists_on_host(&channels_cache_dir) {
                plan.dirs_to_remove.push(channels_cache_dir);
            }
        }

        // Preserve downloads cache, handy for development test cycles

        // Remove $HOME/bin/epkg symlink
        let home_bin_epkg =
            crate::dirs::path_join(&PathBuf::from(&home_dir), &["bin", "epkg"]);
        if lfs::exists_on_host(&home_bin_epkg) {
            plan.symlinks_to_remove.push(home_bin_epkg);
        }

        // Update user shell rc files - only add those that contain epkg content
        let user_shell_rcs = crate::dirs::get_user_shell_rc(&PathBuf::from(&home_dir))?;
        for rc in user_shell_rcs {
            if rc_file_has_epkg(&rc) {
                plan.shell_rc_files.push(rc);
            }
        }
    }

    for ps in crate::dirs::powershell_profile_paths() {
        if lfs::exists_on_host(&ps) {
            let ps_str = ps.to_string_lossy().into_owned();
            if rc_file_has_epkg(&ps_str) {
                plan.shell_rc_files.push(ps_str);
            }
        }
    }

    Ok(plan)
}

fn display_deinit_plan(plan: &DeinitPlan, scope: &str) -> Result<()> {
    println!("\n=== Epkg Deinitialization Plan ({}) ===", scope);

    if !plan.dirs_to_remove.is_empty() {
        println!("\nDirectories to remove:");
        for dir in &plan.dirs_to_remove {
            println!("  {}", dir.display());
        }
    }

    if !plan.symlinks_to_remove.is_empty() {
        println!("\nSymlinks to remove:");
        for symlink in &plan.symlinks_to_remove {
            println!("  {}", symlink.display());
        }
    }

    if !plan.redirect_symlinks.is_empty() {
        println!("\nRedirect symlinks/mountpoints to preserve:");
        for redirect in &plan.redirect_symlinks {
            if lfs::is_symlink(redirect) {
                if let Ok(target) = fs::read_link(redirect) {
                    println!("  {} -> {} (symlink)", redirect.display(), target.display());
                }
            } else {
                println!("  {} (mountpoint)", redirect.display());
            }
        }
        println!("  (User-created redirects will be kept; only contents removed)");
    }

    if !plan.shell_rc_files.is_empty() {
        println!("\nShell configuration files to modify:");
        for rc_file in &plan.shell_rc_files {
            println!("  {}", rc_file);
        }
    }

    if plan.is_empty() {
        println!("No changes required.");
    }

    Ok(())
}

fn confirm_deinit() -> Result<bool> {
    if config().common.dry_run {
        println!("Dry run mode: No changes will be made to the system.");
        return Ok(false);
    }

    if config().common.assume_no {
        return Ok(false);
    }

    if config().common.assume_yes {
        return Ok(true);
    }

    print!("\nDo you want to continue with deinitialization? [y/N] ");
    io::stdout().flush()?;

    let mut input = String::new();
    io::stdin().read_line(&mut input)?;

    let trimmed = input.trim().to_lowercase();
    Ok(trimmed == "y" || trimmed == "yes")
}

fn execute_deinit_plan(plan: &DeinitPlan) -> Result<()> {
    // Remove symlinks
    for symlink in &plan.symlinks_to_remove {
        if lfs::exists_on_host(symlink) {
            println!("Removing symlink: {}", symlink.display());
            lfs::remove_file(symlink)?;
        }
    }

    // Modify shell RC files
    for rc_file in &plan.shell_rc_files {
        remove_epkg_from_rc_file(rc_file)?;
    }

    // Remove directories in the end
    for dir in &plan.dirs_to_remove {
        if lfs::exists_on_host(dir) {
            println!("Removing directory: {}", dir.display());
            force_remove_dir_all_with_progress(dir)
                .wrap_err_with(|| format!("Failed to remove directory: {}", dir.display()))?;
        }
    }

    Ok(())
}

fn format_removal_elapsed(d: Duration) -> String {
    let s = d.as_secs();
    if s < 60 {
        format!("{:.1}s", d.as_secs_f64())
    } else if s < 3600 {
        format!("{}m {}s", s / 60, s % 60)
    } else {
        format!("{}h {}m", s / 3600, (s % 3600) / 60)
    }
}

struct RemovalProgress {
    pb: Option<ProgressBar>,
    last_update: Instant,
    start: Instant,
    removed_count: u64,
    plain_mode: bool,
    verbose: bool,
}

impl RemovalProgress {
    fn new() -> Self {
        let start = Instant::now();
        let verbose = config().common.verbose;
        if config().common.quiet {
            return Self {
                pb: None,
                last_update: start,
                start,
                removed_count: 0,
                plain_mode: false,
                verbose,
            };
        }

        let stderr = std::io::stderr();
        if stderr.is_terminal() {
            let pb = ProgressBar::new_spinner();
            pb.enable_steady_tick(Duration::from_millis(100));
            let style = ProgressStyle::with_template("{spinner:.green} {wide_msg}")
                .expect("hard-coded progress template");
            pb.set_style(style);
            pb.set_message("Removing … 0 entries · 0.0s");
            Self {
                pb: Some(pb),
                last_update: start,
                start,
                removed_count: 0,
                plain_mode: false,
                verbose,
            }
        } else {
            Self {
                pb: None,
                last_update: start,
                start,
                removed_count: 0,
                plain_mode: true,
                verbose,
            }
        }
    }

    fn tick(&mut self) {
        if config().common.quiet {
            return;
        }

        self.removed_count += 1;
        const THROTTLE_OPS_DEFAULT: u64 = 500;
        const THROTTLE_OPS_VERBOSE: u64 = 50;
        let throttle_ops = if self.verbose {
            THROTTLE_OPS_VERBOSE
        } else {
            THROTTLE_OPS_DEFAULT
        };
        let throttle_time = if self.verbose {
            Duration::from_millis(200)
        } else {
            Duration::from_secs(1)
        };
        let now = Instant::now();
        if self.removed_count != 1
            && self.removed_count % throttle_ops != 0
            && now.duration_since(self.last_update) < throttle_time
        {
            return;
        }
        self.last_update = now;

        let elapsed = format_removal_elapsed(self.start.elapsed());
        let msg = format!(
            "Removing … {} entries · {}",
            self.removed_count, elapsed
        );
        if let Some(pb) = &self.pb {
            pb.set_message(msg);
        } else if self.plain_mode {
            eprintln!("{msg}");
        }
    }

    fn finish(&mut self) {
        if let Some(pb) = self.pb.take() {
            pb.finish_and_clear();
        }
    }

    fn abandon(&mut self) {
        if let Some(pb) = self.pb.take() {
            pb.abandon();
        }
    }
}

fn remove_dir_all_recursive_inner(path: &Path, progress: &mut RemovalProgress) -> Result<()> {
    let entries = fs::read_dir(path)
        .wrap_err_with(|| format!("Failed to read directory: {}", path.display()))?;
    for entry in entries {
        let entry = entry.wrap_err_with(|| format!("Failed to read entry in {}", path.display()))?;
        let p = entry.path();
        let meta = lfs::symlink_metadata(&p)
            .wrap_err_with(|| format!("Failed to stat: {}", p.display()))?;
        if meta.file_type().is_symlink() {
            lfs::remove_file(&p)?;
        } else if meta.is_dir() {
            remove_dir_all_recursive_inner(&p, progress)?;
            lfs::remove_dir(&p)?;
        } else {
            lfs::remove_file(&p)?;
        }
        progress.tick();
    }
    Ok(())
}

fn remove_dir_all_recursive_with_progress(path: &Path) -> Result<()> {
    let mut progress = RemovalProgress::new();
    let result = (|| -> Result<()> {
        remove_dir_all_recursive_inner(path, &mut progress)?;
        lfs::remove_dir(path)?;
        progress.tick();
        Ok(())
    })();

    match &result {
        Ok(()) => progress.finish(),
        Err(_) => progress.abandon(),
    }
    result
}

/// Like [`force_remove_dir_all`], but shows indeterminate progress (spinner + entry count and
/// elapsed time) on a TTY unless `--quiet` is set. Updates are throttled (every 500 entries or
/// every second by default; every 50 entries or 200ms with `--verbose`) so terminal I/O stays cheap.
/// With no TTY (e.g. script), defaults to the same fast removal as quiet; pass `--verbose` to force
/// throttled lines on stderr.
pub(crate) fn force_remove_dir_all_with_progress<P: AsRef<Path>>(path: P) -> Result<()> {
    let path = path.as_ref();
    if config().common.quiet {
        return force_remove_dir_all(path);
    }
    if !std::io::stderr().is_terminal() && !config().common.verbose {
        return force_remove_dir_all(path);
    }

    match remove_dir_all_recursive_with_progress(path) {
        Ok(()) => Ok(()),
        Err(_e) => force_remove_dir_all(path),
    }
}

/// Check if an RC file contains epkg initialization markers.
/// Returns true only if both "# epkg begin" and "# epkg end" markers are present.
fn rc_file_has_epkg(rc_file_path: &str) -> bool {
    let path = Path::new(rc_file_path);
    if !lfs::exists_on_host(path) {
        return false;
    }

    match fs::read_to_string(path) {
        Ok(content) => {
            content.contains("# epkg begin") && content.contains("# epkg end")
        }
        Err(_) => false,
    }
}

pub fn remove_epkg_from_rc_file(rc_file_path: &str) -> Result<String> {
    let path = Path::new(rc_file_path);
    if !lfs::exists_on_host(path) {
        return Ok(String::new());
    }

    let content = fs::read_to_string(path)
        .wrap_err_with(|| format!("Failed to read RC file: {}", rc_file_path))?;

    // Check if epkg configuration is present
    if !content.contains("# epkg begin") || !content.contains("# epkg end") {
        return Ok(content);
    }

    // Remove epkg configuration block
    let lines: Vec<&str> = content.lines().collect();
    let mut new_lines = Vec::new();
    let mut in_epkg_block = false;

    for line in lines {
        if line.contains("# epkg begin") {
            in_epkg_block = true;
            continue;
        }
        if line.contains("# epkg end") {
            in_epkg_block = false;
            continue;
        }
        if !in_epkg_block {
            new_lines.push(line);
        }
    }

    let new_content = new_lines.join("\n");

    // Write back the modified content
    lfs::write(path, &new_content)?;

    println!("Removed epkg from shell RC file: {}", rc_file_path);
    Ok(new_content)
}

/// Recursively removes a directory, fixing permission issues if needed.
///
/// Uses eprintln! for informational messages to avoid interfering with shell eval
/// when called from commands like `epkg env remove`.
#[cfg(unix)]
pub fn force_remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
    let path = path.as_ref();

    // First, try normal deletion
    let initial_result = lfs::remove_dir_all(path);
    if initial_result.is_ok() {
        return Ok(());
    }

    // If failed, collect all parent directories of read-only files
    let parent_dirs = find_readonly_dirs(path)?;
    if parent_dirs.is_empty() {
        if let Err(ref e) = initial_result {
            eprintln!(
                "Initial attempt to remove directory '{}' failed: {}",
                path.display(),
                e
            );
        }
        return initial_result.map_err(|e| eyre::eyre!("{}", e));
    }

    eprintln!("Some directories are read-only and cannot be removed automatically.");
    eprintln!("Making {} directories writable...", parent_dirs.len());

    // Make parent directories writable
    for dir in &parent_dirs {
        eprintln!("  - {}", &dir.display());
        utils::fixup_file_permissions(&dir);
    }

    eprintln!("Retrying directory removal after permission fix...");

    // Retry deletion
    match lfs::remove_dir_all(path) {
        Ok(_) => {
            eprintln!("Directory successfully removed after permission fix");
            Ok(())
        }
        Err(e) => {
            eprintln!("Failed to remove directory even after permission fix: {}", e);
            Err(eyre::eyre!("{}", e))
        }
    }
}

/// Simple version for non-Unix platforms (Windows)
#[cfg(not(unix))]
pub fn force_remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
    lfs::remove_dir_all(path.as_ref())
        .map_err(|e| eyre::eyre!("Failed to remove directory: {}", e))
}

/// Finds all read-only directories within the given path.
/// Uses symlink_metadata to avoid following symlinks and prevent infinite recursion
/// on cyclic symlink structures.
#[cfg(unix)]
pub fn find_readonly_dirs<P: AsRef<Path>>(root: P) -> Result<Vec<PathBuf>> {
    let mut readonly_dirs = Vec::new();
    let mut dir_stack = vec![root.as_ref().to_path_buf()];

    while let Some(dir) = dir_stack.pop() {
        // Check if current directory is read-only
        // Use symlink_metadata to avoid following symlinks
        if let Ok(metadata) = lfs::symlink_metadata(&dir) {
            // Skip symlinks - they should be removed separately, not recursed into
            if metadata.file_type().is_symlink() {
                continue;
            }
            if metadata.is_dir() && metadata.permissions().readonly() {
                readonly_dirs.push(dir.clone());
            }
        }

        // Add subdirectories to stack
        // Use symlink_metadata to detect symlinks and avoid following them
        if let Ok(entries) = fs::read_dir(&dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                // Check if it's a real directory (not a symlink pointing to directory)
                if let Ok(meta) = lfs::symlink_metadata(&path) {
                    // Only add real directories to stack; skip symlinks
                    if meta.is_dir() && !meta.file_type().is_symlink() {
                        dir_stack.push(path);
                    }
                }
            }
        }
    }

    // Remove duplicates and sort for consistent output
    readonly_dirs.sort();
    readonly_dirs.dedup();
    Ok(readonly_dirs)
}