use std::fs;
use std::path::PathBuf;
use std::collections::HashMap;
use color_eyre::Result;
use color_eyre::eyre;
use crate::lfs;
use crate::plan::InstallationPlan;
use crate::models::PACKAGE_CACHE;
use crate::install::execute_installation_plan;
use crate::io::{load_installed_packages, load_world};

/// Unlink package files from the environment.
///
/// Validates the pkgline, constructs the store path, and removes all package files
/// from the environment root.
///
/// # Arguments
/// * `pkgkey` - Package key for logging
/// * `pkgline` - Package line (relative path in store)
/// * `store_root` - Root of the package store
/// * `env_root` - Root of the environment
pub fn unlink_package(
    pkgkey: &str,
    pkgline: &str,
    store_root: &PathBuf,
    env_root: &PathBuf,
) -> Result<()> {
    // Validate pkgline
    if pkgline.is_empty() || pkgline.contains("/") || pkgline.contains("..") {
        log::error!("Invalid pkgline for {}: '{}'. Skipping unlink.", pkgkey, pkgline);
        return Err(eyre::eyre!("Invalid pkgline for {}: '{}'", pkgkey, pkgline));
    }

    let pkg_store_path = store_root.join(pkgline);
    log::info!("Unlinking files for package: {} from store path {}", pkgkey, pkg_store_path.display());

    let fs_dir = pkg_store_path.join("fs");
    if !fs_dir.exists() {
        // If the 'fs' directory doesn't exist, it might mean the package was corrupted
        // or never fully extracted. It's safer to log this and return Ok, treating it as if
        // there are no files to unlink, rather than panicking.
        // This can happen if a previous removal was interrupted or if the store is manually altered.
        log::warn!("Package FS root {} does not exist for package directory {}. Assuming no files to unlink.", fs_dir.display(), pkg_store_path.display());
        return Ok(());
    }
    let fs_dir_str = fs_dir.to_str()
        .ok_or_else(|| eyre::eyre!("Invalid path for fs_dir: {}", fs_dir.display()))?;
    let fs_files = crate::utils::list_package_files_with_info(fs_dir_str)?;
    log::debug!("Unlinking package from {} to {} ({} files)", pkg_store_path.display(), env_root.display(), fs_files.len());
    for fs_file_info in fs_files {
        // fs_file_info.path comes from filelist.txt (POSIX); on-disk env paths use PUA on Windows.
        let target_path = env_root.join(lfs::host_path_from_manifest_rel_path(
            fs_file_info.path.trim_start_matches('/'),
        ));

        // Skip symlinks for top-level directories, some are manually created in create_environment_dirs_early()
        // NOTE: On macOS, usr/libexec is a symlink (for brew packages), so we skip it.
        // On Linux, usr/libexec is a real directory (RPM/Debian), so we DON'T skip it.
        #[cfg(target_os = "macos")]
        if matches!(fs_file_info.path.trim_end_matches('/'), "sbin" | "bin" | "lib" | "lib64" | "lib32" | "share" | "include" | "usr/sbin" | "usr/lib64" | "usr/libexec") {
            continue;
        }
        #[cfg(not(target_os = "macos"))]
        if matches!(fs_file_info.path.trim_end_matches('/'), "sbin" | "bin" | "lib" | "lib64" | "lib32" | "share" | "include" | "usr/sbin" | "usr/lib64") {
            continue;
        }

        // Skip dir in source
        if fs_file_info.is_dir() {
            continue;
        }

        // Check if target exists and get its metadata
        if let Ok(target_metadata) = fs::symlink_metadata(&target_path) {
            // Skip if target is a directory (directories are typically shared and shouldn't be removed)
            if target_metadata.is_dir() {
                log::trace!("Skipping directory at target path: {}", target_path.display());
                continue;
            }

            // Remove file (include symlink)
            lfs::remove_file(&target_path)?;
        }
    }

    Ok(())
}

/// Removes specified packages and their orphaned dependencies.
///
/// This function operates solely on the information within `installed_packages`
/// and does not consult external repositories or perform new dependency resolution.
/// This ensures that removal decisions are based purely on the currently recorded
/// state of installed packages.
///
/// The process is as follows:
/// 1. Loads the current set of installed packages.
/// 2. Resolves the input `package_specs` (user-provided package names or keys)
///    against the loaded `installed_packages`. Packages not found are reported.
/// 3. Initializes a `final_removal_set` with packages explicitly matched from `package_specs`.
/// 4. A `processing_queue` is used, seeded with these explicitly requested packages.
/// 5. Iteratively, for each package (`pkg_A`) taken from the queue:
///    a. Its direct dependencies (`pkg_A.depends`) are examined.
///    b. For each dependency (`pkg_B`):
///       i. If `pkg_B` is already in `final_removal_set`, it's skipped.
///       ii. `pkg_B` is NOT automatically removed if it's marked as user-installed
///           (`ebin_exposure` is true or `depend_depth == 0`) or if it's an essential package.
///       iii. Otherwise, `pkg_B` is considered an orphan (and added to `final_removal_set`
///            and the `processing_queue`) if ALL of its recorded reverse dependencies
///            (`pkg_B.rdepends`) are already present in the `final_removal_set`.
///            This means all packages that depend on `pkg_B` are themselves being removed.
/// 6. After the queue is empty, `final_removal_set` contains all packages to be uninstalled.
/// 7. If not a dry run, pre-remove scriptlets are run for these packages.
/// 8. Files for each package in `final_removal_set` are unlinked from the environment.
/// 9. The corresponding entries are removed from `installed_packages`.
/// 10. Post-remove scriptlets are run.
/// 11. A new generation is created, `installed-packages.json` is saved, history recorded,
///     and the 'current' generation symlink is updated.
///
/// This method aims for a safer and more predictable removal process, especially
/// ensuring that shared dependencies are not removed if other installed packages
/// (not part of the current removal set) still rely on them according to the
/// `rdepends` information.
pub fn remove_packages(package_specs: Vec<String>) -> Result<InstallationPlan> {
    load_installed_packages()?;
    load_world()?;

    // Remove packages from world.json based on the specs
    remove_from_world(&package_specs)?;

    let plan = prepare_removal_plan(package_specs)?;

    if plan.ordered_operations.is_empty() {
        return Ok(InstallationPlan::default());
    }

    execute_installation_plan(plan)
}

/// Creates an InstallationPlan for package removal operations.
/// This function handles the dependency resolution and orphan detection logic
/// that was previously embedded in remove_packages().
pub fn prepare_removal_plan(package_specs: Vec<String>) -> Result<InstallationPlan> {
    let mut old_removes = HashMap::new();

    // Step 1: Resolve package specs to package keys
    let (explicitly_requested_keys, _not_found_specs) = resolve_removal_specs(&package_specs);

    // Step 2: Check for blocking dependencies and build old_removes map
    let _prevented_removals = add_top_removable(&mut old_removes, &explicitly_requested_keys);

    // Step 3: Process orphaned dependencies
    add_orphaned_dependencies(&mut old_removes);

    // Step 4: Nothing to install/upgrade
    let new_pkgs = crate::models::InstalledPackagesMap::new();

    // Step 5: Use prepare_installation_plan() with old_removes
    crate::plan::prepare_installation_plan(&new_pkgs, Some(old_removes))
}

/// Remove packages from world.json based on package specs
fn remove_from_world(package_specs: &[String]) -> Result<()> {
    use crate::parse_requires::parse_package_spec_with_version;
    use crate::models::PackageFormat;

    for spec in package_specs {
        // Parse the spec to get package name
        // Use Rpm as default format since most package specs are RPM-based
        // and the format is not available in this context
        let (pkgname, _) = parse_package_spec_with_version(spec, PackageFormat::Rpm);

        // Remove from world.json
        PACKAGE_CACHE.world.write().unwrap().remove(&pkgname);
    }

    Ok(())
}

/// Resolve package specs to package keys for removal
fn resolve_removal_specs(package_specs: &[String]) -> (std::collections::HashSet<String>, Vec<String>) {
    let mut explicitly_requested_keys = std::collections::HashSet::new();
    let mut not_found_specs = Vec::new();

    for spec in package_specs {
        // Try exact match first
        if PACKAGE_CACHE.installed_packages.read().unwrap().contains_key(spec) {
            explicitly_requested_keys.insert(spec.clone());
            continue;
        }
        // Try prefix match (e.g., spec is 'name' and key is 'name__version__arch')
        let mut found_prefix_match = false;
        let installed = PACKAGE_CACHE.installed_packages.read().unwrap();
        for (installed_key, _) in installed.iter() {
            if installed_key.starts_with(&(spec.clone() + "__")) {
                log::info!("Interpreting spec '{}' as '{}' for removal.", spec, installed_key);
                explicitly_requested_keys.insert(installed_key.clone());
                found_prefix_match = true;
                break;
            }
        }
        if !found_prefix_match {
            not_found_specs.push(spec.clone());
        }
    }

    if !not_found_specs.is_empty() {
        println!("Warning: The following specified packages were not found among installed packages:");
        for spec in &not_found_specs {
            println!("- {}", spec);
        }
    }

    if explicitly_requested_keys.is_empty() {
        if package_specs.is_empty() {
            println!("No packages specified for removal.");
        } else {
            println!("No installed packages match the request to remove.");
        }
    }

    (explicitly_requested_keys, not_found_specs)
}

/// Filter out blocking dependencies and populate initial removal plan.
///
/// This function validates whether explicitly requested packages can be safely removed
/// by checking their reverse dependencies (rdepends). A package can only be removed if
/// all of its reverse dependencies are either:
/// - Not currently installed, or
/// - Also explicitly requested for removal
///
/// Packages that pass this check are added to the removal plan.
/// Packages that are blocked by active reverse dependencies are recorded in the
/// prevented_removals map and a warning is displayed to the user.
///
/// # Arguments
/// * `plan` - Mutable reference to the InstallationPlan to populate with removable packages
/// * `explicitly_requested_keys` - Set of package keys that the user wants to remove
///
/// # Returns
/// Map of blocked package keys to their blocking reverse dependencies
fn add_top_removable(
    old_removes: &mut crate::models::InstalledPackagesMap,
    explicitly_requested_keys: &std::collections::HashSet<String>
) -> HashMap<String, Vec<String>> {
    let mut prevented_removals: HashMap<String, Vec<String>> = HashMap::new();

    let installed = PACKAGE_CACHE.installed_packages.read().unwrap();
    for requested_key in explicitly_requested_keys {
        if let Some(requested_pkg_info) = installed.get(requested_key) {
            let mut can_remove_requested_key = true;
            let mut blocking_rdepends = Vec::new();

            for rdep_key in &requested_pkg_info.rdepends {
                // Check if this rdepend is an installed package AND is NOT also being explicitly requested for removal
                if installed.contains_key(rdep_key) && !explicitly_requested_keys.contains(rdep_key) {
                    can_remove_requested_key = false;
                    blocking_rdepends.push(rdep_key.clone());
                }
            }

            if can_remove_requested_key {
                old_removes.insert(requested_key.clone(), requested_pkg_info.clone());
            } else {
                prevented_removals.insert(requested_key.clone(), blocking_rdepends);
            }
        } else {
            // This case should ideally not be hit if explicitly_requested_keys is derived from installed_packages
            // but as a safeguard if spec resolution logic changes.
            log::warn!("Package key '{}' from explicit request not found in installed_packages during removal check.", requested_key);
        }
    }

    if !prevented_removals.is_empty() {
        println!("Warning: The following packages cannot be removed because other installed packages depend on them:");
        for (pkg_to_remove, blockers) in &prevented_removals {
            println!("- '{}' is required by: {}", pkg_to_remove, blockers.join(", "));
        }
        // If ALL explicitly requested packages were blocked, then old_removes will be empty.
        if old_removes.is_empty() {
             println!("No packages will be removed as all specified packages have active dependencies or were not found.");
        }
        // If some were blocked but others were not, old_removes is not empty,
        // and we proceed with the ones that can be removed. The warning above is sufficient.
    }

    if old_removes.is_empty() && !explicitly_requested_keys.is_empty() {
        println!("No packages will be removed.");
    }

    prevented_removals
}

/// Find orphaned dependencies recursively and adds them to the removal plan.
///
/// This function identifies and marks dependencies as orphaned when all packages
/// that depend on them are being removed. It operates on packages already marked
/// for removal in the plan and recursively checks their dependencies.
///
/// The algorithm works as follows:
/// 1. Starts with a queue of packages already marked for removal in `plan.old_removes`.
/// 2. For each package being removed, examines its direct dependencies.
/// 3. For each dependency, determines if it should be considered orphaned:
///    - Skips if already marked for removal
///    - Skips if explicitly installed (`ebin_exposure == true`) or has `depend_depth == 0`
///    - Skips if it's an essential package
///    - Marks as orphaned if ALL of its reverse dependencies (`rdepends`) are
///      already in the removal plan (meaning all packages that depend on it are
///      being removed)
/// 4. When a dependency is marked as orphaned, it's added to the removal plan
///    and to the processing queue to check its own dependencies recursively.
///
/// This ensures that transitive dependencies are properly cleaned up when
/// they're no longer needed by any remaining installed packages.
///
/// # Arguments
/// * `plan` - Mutable reference to the InstallationPlan that contains packages
///            already marked for removal and will be updated with orphaned dependencies
fn add_orphaned_dependencies(old_removes: &mut crate::models::InstalledPackagesMap) {
    // Build initial processing queue from packages already marked for removal
    let mut processing_queue: Vec<String> = old_removes.keys().cloned().collect();
    let mut visited_for_orphan_check = std::collections::HashSet::new();

    while let Some(pkgkey_being_removed) = processing_queue.pop() {
        if !visited_for_orphan_check.insert(pkgkey_being_removed.clone()) {
            continue;
        }

        let installed = PACKAGE_CACHE.installed_packages.read().unwrap();
        let current_pkg_info = match installed.get(&pkgkey_being_removed) {
            Some(info) => info.clone(),
            None => continue,
        };

        for dep_pkgkey in &current_pkg_info.depends {
            if old_removes.contains_key(dep_pkgkey) {
                continue; // Already marked for removal
            }

            log::debug!("[Orphan Check] Considering dependency '{}' of package '{}' being removed.", dep_pkgkey, pkgkey_being_removed);
            let dep_info = match installed.get(dep_pkgkey) {
                Some(info) => info,
                None => {
                    log::warn!("Dependency '{}' of '{}' not found in installed_packages. Skipping orphan check.", dep_pkgkey, pkgkey_being_removed);
                    continue;
                }
            };

            log::debug!("[Orphan Check] Properties for dep '{}': ebin_exposure={}, depend_depth={}, rdepends={:?}", dep_pkgkey, dep_info.ebin_exposure, dep_info.depend_depth, dep_info.rdepends);

            if dep_info.ebin_exposure || dep_info.depend_depth == 0 {
                log::debug!("Dependency '{}' is explicitly installed or depth 0, not removing automatically.", dep_pkgkey);
                continue;
            }

            let dep_pkgname_parts: Vec<&str> = dep_pkgkey.split("__").collect();
            let dep_pkgname = dep_pkgname_parts.get(0).unwrap_or(&"");
            if crate::mmio::is_essential_pkgname(dep_pkgname) {
                log::debug!("Dependency '{}' ({}) is essential, not removing automatically.", dep_pkgkey, dep_pkgname);
                continue;
            }

            // Check if all reverse dependencies are being removed
            let mut all_rdepends_being_removed = true;
            if dep_info.rdepends.is_empty() {
                // If a non-ebin package has no rdepends, it's an orphan if its direct requirer (pkgkey_being_removed) is removed.
                // However, this state implies its rdepends were not properly recorded or it's a very old package.
                // For safety, we only act if there's at least one rdepend and it's pkgkey_being_removed or also in final_removal_set.
                // If rdepends is truly empty, it means no *other* installed package depends on it.
                // If pkgkey_being_removed was its only dependent, it becomes an orphan.
                // This is implicitly handled: if no other rdepend exists that is *not* being removed, it's an orphan.
                log::debug!("Dependency '{}' has no recorded rdepends. It will be removed if not kept by other means.", dep_pkgkey);
            } else {
                for rdep_pkgkey in &dep_info.rdepends {
                    let rdep_is_in_final_set = old_removes.contains_key(rdep_pkgkey);
                    log::debug!("[Orphan Check]   Checking rdepend '{}' (of dep '{}'): is_in_final_removal_set? {}", rdep_pkgkey, dep_pkgkey, rdep_is_in_final_set);
                    if !rdep_is_in_final_set {
                        all_rdepends_being_removed = false;
                        log::debug!("Dependency '{}' will be kept because its rdepend '{}' is not being removed.", dep_pkgkey, rdep_pkgkey);
                        break;
                    }
                }
            }

            log::debug!("[Orphan Check] Final all_rdepends_being_removed for dep '{}': {}", dep_pkgkey, all_rdepends_being_removed);
            if all_rdepends_being_removed {
                log::info!("Marking orphaned dependency for removal: {}", dep_pkgkey);
                old_removes.insert(dep_pkgkey.clone(), dep_info.clone());
                // Add to queue only if not already processed to check its dependencies for orphaning
                if !visited_for_orphan_check.contains(dep_pkgkey) {
                     processing_queue.push(dep_pkgkey.clone());
                }
            }
        }
    }
}