// src/build/cask/artifacts/suite.rs

use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Implements the `suite` stanza by moving each named directory from
/// the staging area into `/Applications`, then symlinking it in the Caskroom.
///
/// Mirrors Homebrew’s Suite < Moved behavior (dirmethod :appdir)
/// :contentReference[oaicite:3]{index=3}

pub fn install_suite(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def.iter() {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("suite").and_then(|v| v.as_array()) {
                    for entry in entries {
                        if let Some(dir_name) = entry.as_str() {
                            let src = stage_path.join(dir_name);
                            if !src.exists() {
                                debug!(
                                    "Suite directory '{}' not found in staging, skipping",
                                    dir_name
                                );
                                continue;
                            }
                            let dest_dir = config.applications_dir();
                            let dest = dest_dir.join(dir_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }
                            debug!("Moving suite '{}' → '{}'", src.display(), dest.display());
                            let mv_status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !mv_status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }
                            installed.push(InstalledArtifact::AppBundle { path: dest.clone() });
                            let link = cask_version_install_path.join(dir_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break;
                }
            }
        }
    }
    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/dictionary.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Implements the `dictionary` stanza by moving each declared
/// `.dictionary` bundle from the staging area into `~/Library/Dictionaries`,
/// then symlinking it in the Caskroom.
///
/// Homebrew’s Ruby definition is simply:
/// ```ruby
/// class Dictionary < Moved; end
/// ```
/// :contentReference[oaicite:2]{index=2}
pub fn install_dictionary(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Find any `dictionary` arrays in the raw JSON artifacts
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("dictionary").and_then(|v| v.as_array()) {
                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Dictionary bundle '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            // Standard user dictionary directory: ~/Library/Dictionaries
                            // :contentReference[oaicite:3]{index=3}
                            let dest_dir = config
                                .home_dir() // e.g. /Users/alxknt
                                .join("Library")
                                .join("Dictionaries");
                            fs::create_dir_all(&dest_dir)?;

                            let dest = dest_dir.join(bundle_name);
                            // Remove any previous install
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Moving dictionary '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try a direct move; fall back to recursive copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record the moved bundle
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink back into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // Only one `dictionary` stanza per cask
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/colorpicker.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs any `colorpicker` stanzas from the Cask definition.
///
/// Homebrew’s `Colorpicker` artifact simply subclasses `Moved` with
/// `dirmethod :colorpickerdir` → `~/Library/ColorPickers` :contentReference[oaicite:3]{index=3}.
pub fn install_colorpicker(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("colorpicker").and_then(|v| v.as_array()) {
                    // For each declared bundle name:
                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Colorpicker bundle '{}' not found in stage; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            // Ensure ~/Library/ColorPickers exists
                            // :contentReference[oaicite:4]{index=4}
                            let dest_dir = config
                                .home_dir() // e.g. /Users/alxknt
                                .join("Library")
                                .join("ColorPickers");
                            fs::create_dir_all(&dest_dir)?;

                            let dest = dest_dir.join(bundle_name);
                            // Remove any previous copy
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Moving colorpicker '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // mv, fallback to cp -R if necessary (cross‑device)
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record as a moved artifact (bundle installed)
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink back into Caskroom for reference
                            // :contentReference[oaicite:5]{index=5}
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // only one `colorpicker` stanza per cask
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/prefpane.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Implements the `prefpane` stanza by moving each declared
/// preference pane bundle from the staging area into
/// `~/Library/PreferencePanes`, then symlinking it in the Caskroom.
///
/// Mirrors Homebrew’s `Prefpane < Moved` pattern.
pub fn install_prefpane(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Look for "prefpane" entries in the JSON artifacts
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("prefpane").and_then(|v| v.as_array()) {
                    // Target directory for user preference panes
                    let dest_dir = config.home_dir().join("Library").join("PreferencePanes");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Preference pane '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing prefpane '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved bundle
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single stanza
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/font.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Implements the `font` stanza by moving each declared
/// font file or directory from the staging area into
/// `~/Library/Fonts`, then symlinking it in the Caskroom.
///
/// Mirrors Homebrew’s `Dictionary < Moved` and `Colorpicker < Moved` pattern.
pub fn install_font(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Look for "font" entries in the JSON artifacts
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("font").and_then(|v| v.as_array()) {
                    // Target directory for user fonts
                    let dest_dir = config.home_dir().join("Library").join("Fonts");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(name) = entry.as_str() {
                            let src = stage_path.join(name);
                            if !src.exists() {
                                debug!("Font '{}' not found in staging; skipping", name);
                                continue;
                            }

                            let dest = dest_dir.join(name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!("Installing font '{}' → '{}'", src.display(), dest.display());
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved font
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single font stanza per cask
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/service.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers;

/// Installs `service` artifacts by moving each declared
/// Automator workflow or service bundle from the staging area into
/// `~/Library/Services`, then symlinking it in the Caskroom.
///
/// Mirrors Homebrew’s `Service < Moved` behavior.
pub fn install_service(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("service").and_then(|v| v.as_array()) {
                    // Target directory for user Services
                    let dest_dir = config.home_dir().join("Library").join("Services");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Service bundle '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = helpers::remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing service '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved service
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = helpers::remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // one stanza only
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/internet_plugin.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Implements the `internet_plugin` stanza by moving each declared
/// internet plugin bundle from the staging area into
/// `~/Library/Internet Plug-Ins`, then symlinking it in the Caskroom.
///
/// Mirrors Homebrew’s `InternetPlugin < Moved` pattern.
pub fn install_internet_plugin(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Look for "internet_plugin" entries in the JSON artifacts
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("internet_plugin").and_then(|v| v.as_array()) {
                    // Target directory for user internet plugins
                    let dest_dir = config.home_dir().join("Library").join("Internet Plug-Ins");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(name) = entry.as_str() {
                            let src = stage_path.join(name);
                            if !src.exists() {
                                debug!("Internet plugin '{}' not found in staging; skipping", name);
                                continue;
                            }

                            let dest = dest_dir.join(name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing internet plugin '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved plugin
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single stanza
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/vst3_plugin.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `vst3_plugin` bundles from the staging area into
/// `~/Library/Audio/Plug-Ins/VST3`, then symlinks them into the Caskroom.
///
/// Mirrors Homebrew’s `Vst3Plugin < Moved` pattern.
pub fn install_vst3_plugin(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("vst3_plugin").and_then(|v| v.as_array()) {
                    // Target directory for VST3 plugins
                    let dest_dir = config
                        .home_dir()
                        .join("Library")
                        .join("Audio")
                        .join("Plug-Ins")
                        .join("VST3");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "VST3 plugin '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing VST3 plugin '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved plugin
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = crate::install::cask::helpers::remove_path_robustly(
                                &link, config, true,
                            );
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single stanza
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/qlplugin.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `qlplugin` bundles from the staging area into
/// `~/Library/QuickLook`, then symlinks them into the Caskroom.
///
/// Mirrors Homebrew’s `QuickLook < Moved` pattern for QuickLook plugins.
pub fn install_qlplugin(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Look for "qlplugin" entries in the JSON artifacts
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("qlplugin").and_then(|v| v.as_array()) {
                    // Target directory for QuickLook plugins
                    let dest_dir = config.home_dir().join("Library").join("QuickLook");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "QuickLook plugin '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing QuickLook plugin '{}' → '{}',",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved plugin
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single stanza
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/screen_saver.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `screen_saver` bundles from the staging area into
/// `~/Library/Screen Savers`, then symlinks them into the Caskroom.
///
/// Mirrors Homebrew’s `ScreenSaver < Moved` pattern.
pub fn install_screen_saver(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("screen_saver").and_then(|v| v.as_array()) {
                    // Target directory for user screen savers
                    let dest_dir = config.home_dir().join("Library").join("Screen Savers");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Screen saver '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing screen saver '{}' → '{}',",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved screen saver
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single stanza
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/mdimporter.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `mdimporter` bundles from the staging area into
/// `~/Library/Spotlight`, then symlinks them into the Caskroom,
/// and reloads them via `mdimport -r` so Spotlight picks them up.
///
/// Mirrors Homebrew’s `Mdimporter < Moved` behavior.
pub fn install_mdimporter(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("mdimporter").and_then(|v| v.as_array()) {
                    // Target directory for user Spotlight importers
                    let dest_dir = config.home_dir().join("Library").join("Spotlight");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Mdimporter bundle '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing mdimporter '{}' → '{}',",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved importer
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });

                            // Reload Spotlight importer so it's picked up immediately
                            debug!("Reloading Spotlight importer: {}", dest.display());
                            let _ = Command::new("/usr/bin/mdimport")
                                .arg("-r")
                                .arg(&dest)
                                .status();
                        }
                    }
                    break; // one stanza only
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/vst_plugin.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `vst_plugin` bundles from the staging area into
/// `~/Library/Audio/Plug-Ins/VST`, then symlinks them into the Caskroom.
///
/// Mirrors Homebrew’s `VstPlugin < Moved` pattern.
pub fn install_vst_plugin(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("vst_plugin").and_then(|v| v.as_array()) {
                    // Target directory for VST plugins
                    let dest_dir = config
                        .home_dir()
                        .join("Library")
                        .join("Audio")
                        .join("Plug-Ins")
                        .join("VST");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "VST plugin '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing VST plugin '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved plugin
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // single stanza
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/audio_unit_plugin.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `audio_unit_plugin` bundles from the staging area into
/// `~/Library/Audio/Plug-Ins/Components`, then symlinks them into the Caskroom.
///
/// Mirrors Homebrew’s `AudioUnitPlugin < Moved` pattern.
pub fn install_audio_unit_plugin(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("audio_unit_plugin").and_then(|v| v.as_array()) {
                    // Target directory for Audio Unit components
                    let dest_dir = config
                        .home_dir()
                        .join("Library")
                        .join("Audio")
                        .join("Plug-Ins")
                        .join("Components");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "AudioUnit plugin '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing AudioUnit plugin '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved plugin
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom for reference
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // one stanza only
                }
            }
        }
    }

    Ok(installed)
}

// ===== src/build/cask/artifacts/manpage.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::sync::LazyLock;

use regex::Regex;
use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

// --- Moved Regex Creation Outside ---
static MANPAGE_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\.([1-8nl])(?:\.gz)?$").unwrap());

/// Install any `manpage` stanzas from the Cask definition.
/// Mirrors Homebrew’s `Cask::Artifact::Manpage < Symlinked` behavior
/// :contentReference[oaicite:3]{index=3}.
pub fn install_manpage(
    cask: &Cask,
    stage_path: &Path,
    _cask_version_install_path: &Path, // Not needed for symlinking manpages
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Look up the "manpage" array in the raw artifacts JSON :contentReference[oaicite:4]{index=4}
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("manpage").and_then(|v| v.as_array()) {
                    for entry in entries {
                        if let Some(man_file) = entry.as_str() {
                            let src = stage_path.join(man_file);
                            if !src.exists() {
                                debug!(
                                    "Manpage '{}' not found in staging area, skipping",
                                    man_file
                                );
                                continue;
                            }

                            // Use the static regex
                            let section = if let Some(caps) = MANPAGE_RE.captures(man_file) {
                                caps.get(1).unwrap().as_str()
                            } else {
                                debug!(
                                    "Filename '{}' does not look like a manpage, skipping",
                                    man_file
                                );
                                continue;
                            };

                            // Build the target directory: e.g. /opt/sps/share/man/man1
                            let man_dir = config.man_base_dir().join(format!("man{section}"));
                            fs::create_dir_all(&man_dir)?;

                            // Determine the target path
                            let file_name = Path::new(man_file).file_name().ok_or_else(|| {
                                sps_common::error::SpsError::Generic(format!(
                                    "Invalid manpage filename: {man_file}"
                                ))
                            })?; // Handle potential None
                            let dest = man_dir.join(file_name);

                            // Remove any existing file or symlink
                            // :contentReference[oaicite:7]{index=7}
                            if dest.exists() || dest.symlink_metadata().is_ok() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!("Linking manpage '{}' → '{}'", src.display(), dest.display());
                            // Create the symlink
                            symlink(&src, &dest)?;

                            // Record it in our manifest
                            installed.push(InstalledArtifact::ManpageLink {
                                link_path: dest.clone(),
                                target_path: src.clone(),
                            });
                        }
                    }
                    // Assume only one "manpage" stanza per Cask based on Homebrew structure
                    break;
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/keyboard_layout.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Mirrors Homebrew’s `KeyboardLayout < Moved` behavior.
pub fn install_keyboard_layout(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("keyboard_layout").and_then(|v| v.as_array()) {
                    // Target directory for user keyboard layouts
                    let dest_dir = config.home_dir().join("Library").join("Keyboard Layouts");
                    fs::create_dir_all(&dest_dir)?;

                    for entry in entries {
                        if let Some(bundle_name) = entry.as_str() {
                            let src = stage_path.join(bundle_name);
                            if !src.exists() {
                                debug!(
                                    "Keyboard layout '{}' not found in staging; skipping",
                                    bundle_name
                                );
                                continue;
                            }

                            let dest = dest_dir.join(bundle_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }

                            debug!(
                                "Installing keyboard layout '{}' → '{}'",
                                src.display(),
                                dest.display()
                            );
                            // Try move, fallback to copy
                            let status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }

                            // Record moved bundle
                            installed.push(InstalledArtifact::MovedResource { path: dest.clone() });

                            // Symlink into Caskroom
                            let link = cask_version_install_path.join(bundle_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break; // one stanza only
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/binary.rs =====

use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::helpers::remove_path_robustly;

/// Installs `binary` artifacts, which can be declared as:
///  - a simple string: `"foo"` (source and target both `"foo"`)
///  - a map: `{ "source": "path/in/stage", "target": "name", "chmod": "0755" }`
///  - a map with just `"target"`: automatically generate a wrapper script
///
/// Copies or symlinks executables into the prefix bin directory,
/// and records both the link and caskroom reference.
pub fn install_binary(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("binary") {
                    // Normalize into an array
                    let arr = if let Some(arr) = entries.as_array() {
                        arr.clone()
                    } else {
                        vec![entries.clone()]
                    };

                    let bin_dir = config.bin_dir();
                    fs::create_dir_all(&bin_dir)?;

                    for entry in arr {
                        // Determine source, target, and optional chmod
                        let (source_rel, target_name, chmod) = if let Some(tgt) = entry.as_str() {
                            // simple form: "foo"
                            (tgt.to_string(), tgt.to_string(), None)
                        } else if let Some(m) = entry.as_object() {
                            let target = m
                                .get("target")
                                .and_then(|v| v.as_str())
                                .map(String::from)
                                .ok_or_else(|| {
                                    SpsError::InstallError(format!(
                                        "Binary artifact missing 'target': {m:?}"
                                    ))
                                })?;

                            let chmod = m.get("chmod").and_then(|v| v.as_str()).map(String::from);

                            // If `source` is provided, use it; otherwise generate wrapper
                            let source = if let Some(src) = m.get("source").and_then(|v| v.as_str())
                            {
                                src.to_string()
                            } else {
                                // generate wrapper script in caskroom
                                let wrapper_name = format!("{target}.wrapper.sh");
                                let wrapper_path = cask_version_install_path.join(&wrapper_name);

                                // assume the real executable lives inside the .app bundle
                                let app_name = format!("{}.app", cask.display_name());
                                let exe_path =
                                    format!("/Applications/{app_name}/Contents/MacOS/{target}");

                                let script =
                                    format!("#!/usr/bin/env bash\nexec \"{exe_path}\" \"$@\"\n");
                                fs::write(&wrapper_path, script)?;
                                Command::new("chmod")
                                    .arg("+x")
                                    .arg(&wrapper_path)
                                    .status()?;

                                wrapper_name
                            };

                            (source, target, chmod)
                        } else {
                            debug!("Invalid binary artifact entry: {:?}", entry);
                            continue;
                        };

                        let src_path = stage_path.join(&source_rel);
                        if !src_path.exists() {
                            debug!("Binary source '{}' not found, skipping", src_path.display());
                            continue;
                        }

                        // Link into bin_dir
                        let link_path = bin_dir.join(&target_name);
                        let _ = fs::remove_file(&link_path);
                        debug!(
                            "Linking binary '{}' → '{}'",
                            src_path.display(),
                            link_path.display()
                        );
                        symlink(&src_path, &link_path)?;

                        // Apply chmod if specified
                        if let Some(mode) = chmod.as_deref() {
                            let _ = Command::new("chmod").arg(mode).arg(&link_path).status();
                        }

                        installed.push(InstalledArtifact::BinaryLink {
                            link_path: link_path.clone(),
                            target_path: src_path.clone(),
                        });

                        // Also create a Caskroom symlink for reference
                        let caskroom_link = cask_version_install_path.join(&target_name);
                        let _ = remove_path_robustly(&caskroom_link, config, true);
                        symlink(&link_path, &caskroom_link)?;
                        installed.push(InstalledArtifact::CaskroomLink {
                            link_path: caskroom_link,
                            target_path: link_path.clone(),
                        });
                    }

                    // Only one binary stanza per cask
                    break;
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/installer.rs =====

use std::path::Path;
use std::process::{Command, Stdio};

use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::InstalledArtifact;

// Helper to validate that the executable is a filename (relative, no '/' or "..")
fn validate_filename_or_relative_path(file: &str) -> Result<String> {
    if file.starts_with("/") || file.contains("..") || file.contains("/") {
        return Err(SpsError::Generic(format!(
            "Invalid executable filename: {file}"
        )));
    }
    Ok(file.to_string())
}

// Helper to validate a command argument based on allowed characters or allowed option form
fn validate_argument(arg: &str) -> Result<String> {
    if arg.starts_with("-") {
        return Ok(arg.to_string());
    }
    if arg.starts_with("/") || arg.contains("..") || arg.contains("/") {
        return Err(SpsError::Generic(format!("Invalid argument: {arg}")));
    }
    if !arg
        .chars()
        .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
    {
        return Err(SpsError::Generic(format!(
            "Invalid characters in argument: {arg}"
        )));
    }
    Ok(arg.to_string())
}

/// Implements the `installer` stanza:
/// - `manual`: prints instructions to open the staged path.
/// - `script`: runs the given executable with args, under sudo if requested.
///
/// Mirrors Homebrew’s `Cask::Artifact::Installer` behavior :contentReference[oaicite:1]{index=1}.
pub fn run_installer(
    cask: &Cask,
    stage_path: &Path,
    _cask_version_install_path: &Path,
    _config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Find the `installer` definitions in the raw JSON artifacts
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def {
            if let Some(obj) = art.as_object() {
                if let Some(insts) = obj.get("installer").and_then(|v| v.as_array()) {
                    for inst in insts {
                        if let Some(inst_obj) = inst.as_object() {
                            if let Some(man) = inst_obj.get("manual").and_then(|v| v.as_str()) {
                                debug!(
                                    "Cask {} requires manual install. To finish:\n    open {}",
                                    cask.token,
                                    stage_path.join(man).display()
                                );
                                continue;
                            }
                            let exe_key = if inst_obj.contains_key("script") {
                                "script"
                            } else {
                                "executable"
                            };
                            let executable = inst_obj
                                .get(exe_key)
                                .and_then(|v| v.as_str())
                                .ok_or_else(|| {
                                    SpsError::Generic(format!(
                                        "installer stanza missing '{exe_key}' field"
                                    ))
                                })?;
                            let args: Vec<String> = inst_obj
                                .get("args")
                                .and_then(|v| v.as_array())
                                .map(|arr| {
                                    arr.iter()
                                        .filter_map(|a| a.as_str().map(String::from))
                                        .collect()
                                })
                                .unwrap_or_default();
                            let use_sudo = inst_obj
                                .get("sudo")
                                .and_then(|v| v.as_bool())
                                .unwrap_or(false);

                            let validated_executable =
                                validate_filename_or_relative_path(executable)?;
                            let mut validated_args = Vec::new();
                            for arg in &args {
                                validated_args.push(validate_argument(arg)?);
                            }

                            let script_path = stage_path.join(&validated_executable);
                            if !script_path.exists() {
                                return Err(SpsError::NotFound(format!(
                                    "Installer script not found: {}",
                                    script_path.display()
                                )));
                            }

                            debug!(
                                "Running installer script '{}' for cask {}",
                                script_path.display(),
                                cask.token
                            );
                            let mut cmd = if use_sudo {
                                let mut c = Command::new("sudo");
                                c.arg(script_path.clone());
                                c
                            } else {
                                Command::new(script_path.clone())
                            };
                            cmd.args(&validated_args);
                            cmd.stdin(Stdio::null())
                                .stdout(Stdio::inherit())
                                .stderr(Stdio::inherit());

                            let status = cmd.status().map_err(|e| {
                                SpsError::Generic(format!("Failed to spawn installer script: {e}"))
                            })?;
                            if !status.success() {
                                return Err(SpsError::InstallError(format!(
                                    "Installer script exited with {status}"
                                )));
                            }

                            installed
                                .push(InstalledArtifact::CaskroomReference { path: script_path });
                        }
                    }
                }
            }
        }
    }

    Ok(installed)
}

// ===== sps-core/src/build/cask/artifacts/input_method.rs =====

use std::fs;
use std::os::unix::fs as unix_fs;
use std::path::Path;

use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;

use crate::install::cask::helpers::remove_path_robustly;
use crate::install::cask::write_cask_manifest;

/// Install `input_method` artifacts from the staged directory into
/// `~/Library/Input Methods` and record installed artifacts.
pub fn install_input_method(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();

    // Ensure we have an array of input_method names
    if let Some(artifacts_def) = &cask.artifacts {
        for artifact_value in artifacts_def {
            if let Some(obj) = artifact_value.as_object() {
                if let Some(names) = obj.get("input_method").and_then(|v| v.as_array()) {
                    for name_val in names {
                        if let Some(name) = name_val.as_str() {
                            let source = stage_path.join(name);
                            if source.exists() {
                                // Target directory: ~/Library/Input Methods
                                let target_dir =
                                    config.home_dir().join("Library").join("Input Methods");
                                if !target_dir.exists() {
                                    fs::create_dir_all(&target_dir)?;
                                }
                                let target = target_dir.join(name);

                                // Remove existing input method if present
                                if target.exists() {
                                    let _ = remove_path_robustly(&target, config, true);
                                }

                                // Move (or rename) the staged bundle
                                fs::rename(&source, &target)
                                    .or_else(|_| unix_fs::symlink(&source, &target))?;

                                // Record the main artifact
                                installed.push(InstalledArtifact::MovedResource {
                                    path: target.clone(),
                                });

                                // Create a caskroom symlink for uninstallation
                                let link_path = cask_version_install_path.join(name);
                                if link_path.exists() {
                                    let _ = remove_path_robustly(&link_path, config, true);
                                }
                                #[cfg(unix)]
                                std::os::unix::fs::symlink(&target, &link_path)?;

                                installed.push(InstalledArtifact::CaskroomLink {
                                    link_path,
                                    target_path: target,
                                });
                            }
                        }
                    }
                }
            }
        }
    }

    // Write manifest for these artifacts
    write_cask_manifest(cask, cask_version_install_path, installed.clone())?;
    Ok(installed)
}

// sps-core/src/uninstall/cask.rs
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use lazy_static::lazy_static;
use regex::Regex;
// serde_json is used for CaskInstallManifest deserialization
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::{Cask, ZapActionDetail};
use tracing::{debug, error, warn};
use trash; // This will be used by trash_path

// Import helpers from the common module within the uninstall scope
use super::common::{expand_tilde, is_safe_path, remove_filesystem_artifact};
use crate::check::installed::InstalledPackageInfo;
// Corrected import path if install::cask::helpers is where it lives now
use crate::install::cask::helpers::{
    cleanup_empty_parent_dirs_in_private_store,
    remove_path_robustly as remove_path_robustly_from_install_helpers,
};
use crate::install::cask::CaskInstallManifest;
#[cfg(target_os = "macos")]
use crate::utils::applescript;

lazy_static! {
    static ref VALID_PKGID_RE: Regex = Regex::new(r"^[a-zA-Z0-9._-]+$").unwrap();
    static ref VALID_LABEL_RE: Regex = Regex::new(r"^[a-zA-Z0-9._-]+$").unwrap();
    static ref VALID_SCRIPT_PATH_RE: Regex = Regex::new(r"^[a-zA-Z0-9/._-]+$").unwrap();
    static ref VALID_SIGNAL_RE: Regex = Regex::new(r"^[A-Z0-9]+$").unwrap();
    static ref VALID_BUNDLE_ID_RE: Regex =
        Regex::new(r"^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$").unwrap();
}

/// Performs a "soft" uninstall for a Cask.
/// It processes the `CASK_INSTALL_MANIFEST.json` to remove linked artifacts
/// and then updates the manifest to mark the cask as not currently installed.
/// The Cask's versioned directory in the Caskroom is NOT removed.
pub fn uninstall_cask_artifacts(info: &InstalledPackageInfo, config: &Config) -> Result<()> {
    debug!(
        "Soft uninstalling Cask artifacts for {} version {}",
        info.name, info.version
    );
    let manifest_path = info.path.join("CASK_INSTALL_MANIFEST.json");
    let mut removal_errors: Vec<String> = Vec::new();

    if manifest_path.is_file() {
        debug!(
            "Processing manifest for soft uninstall: {}",
            manifest_path.display()
        );
        match fs::read_to_string(&manifest_path) {
            Ok(manifest_str) => match serde_json::from_str::<CaskInstallManifest>(&manifest_str) {
                Ok(mut manifest) => {
                    if !manifest.is_installed {
                        debug!("Cask {} version {} is already marked as uninstalled in manifest. Nothing to do for soft uninstall.", info.name, info.version);
                        return Ok(());
                    }

                    debug!(
                        "Soft uninstalling {} artifacts listed in manifest for {} {}...",
                        manifest.artifacts.len(),
                        info.name,
                        info.version
                    );
                    for artifact in manifest.artifacts.iter().rev() {
                        if !process_artifact_uninstall_core(artifact, config, false) {
                            removal_errors.push(format!("Failed to remove artifact: {artifact:?}"));
                        }
                    }

                    manifest.is_installed = false;
                    match fs::File::create(&manifest_path) {
                        Ok(file) => {
                            let writer = std::io::BufWriter::new(file);
                            if let Err(e) = serde_json::to_writer_pretty(writer, &manifest) {
                                warn!(
                                    "Failed to update manifest {}: {}",
                                    manifest_path.display(),
                                    e
                                );
                            } else {
                                debug!(
                                    "Manifest updated successfully for soft uninstall: {}",
                                    manifest_path.display()
                                );
                            }
                        }
                        Err(e) => {
                            warn!(
                                "Failed to open manifest for writing (soft uninstall) at {}: {}",
                                manifest_path.display(),
                                e
                            );
                        }
                    }
                }
                Err(e) => warn!(
                    "Failed to parse cask manifest {}: {}. Cannot perform detailed soft uninstall.",
                    manifest_path.display(),
                    e
                ),
            },
            Err(e) => warn!(
                "Failed to read cask manifest {}: {}. Cannot perform detailed soft uninstall.",
                manifest_path.display(),
                e
            ),
        }
    } else {
        warn!(
            "No CASK_INSTALL_MANIFEST.json found in {}. Cannot perform detailed soft uninstall for {} {}.",
            info.path.display(),
            info.name,
            info.version
        );
    }

    if removal_errors.is_empty() {
        Ok(())
    } else {
        Err(SpsError::InstallError(format!(
            "Errors during cask artifact soft removal for {}: {}",
            info.name,
            removal_errors.join("; ")
        )))
    }
}

/// Performs a "zap" uninstall for a Cask, removing files defined in `zap` stanzas
/// and cleaning up the private store. Also marks the cask as uninstalled in its manifest.
pub async fn zap_cask_artifacts(
    info: &InstalledPackageInfo,
    cask_def: &Cask,
    config: &Config,
) -> Result<()> {
    debug!("Starting ZAP process for cask: {}", cask_def.token);
    let home = config.home_dir();
    let cask_version_path_in_caskroom = &info.path;
    let mut zap_errors: Vec<String> = Vec::new();

    let mut primary_app_name_from_manifest: Option<String> = None;
    let manifest_path = cask_version_path_in_caskroom.join("CASK_INSTALL_MANIFEST.json");

    if manifest_path.is_file() {
        match fs::read_to_string(&manifest_path) {
            Ok(manifest_str) => match serde_json::from_str::<CaskInstallManifest>(&manifest_str) {
                Ok(mut manifest) => {
                    primary_app_name_from_manifest = manifest.primary_app_file_name.clone();
                    if manifest.is_installed {
                        manifest.is_installed = false;
                        if let Ok(file) = fs::File::create(&manifest_path) {
                            let writer = std::io::BufWriter::new(file);
                            if let Err(e) = serde_json::to_writer_pretty(writer, &manifest) {
                                warn!(
                                    "Failed to update manifest during zap for {}: {}",
                                    manifest_path.display(),
                                    e
                                );
                            }
                        } else {
                            warn!(
                                "Failed to open manifest for writing during zap at {}",
                                manifest_path.display()
                            );
                        }
                    }
                }
                Err(e) => warn!(
                    "Failed to parse manifest during zap {}: {}",
                    manifest_path.display(),
                    e
                ),
            },
            Err(e) => warn!(
                "Failed to read manifest during zap {}: {}",
                manifest_path.display(),
                e
            ),
        }
    } else {
        warn!("No manifest found at {} during zap. Private store cleanup might be incomplete if app name changed.", manifest_path.display());
    }

    if !cleanup_private_store(
        &cask_def.token,
        &info.version,
        primary_app_name_from_manifest.as_deref(),
        config,
    ) {
        let msg = format!(
            "Failed to clean up private store for cask {} version {}",
            cask_def.token, info.version
        );
        warn!("{}", msg);
        zap_errors.push(msg);
    }

    let zap_stanzas = match &cask_def.zap {
        Some(stanzas) => stanzas,
        None => {
            debug!("No zap stanza found for cask {}", cask_def.token);
            // Proceed to Caskroom cleanup even if no specific zap actions
            if !remove_filesystem_artifact(cask_version_path_in_caskroom, true) {
                // use_sudo = true for Caskroom
                if cask_version_path_in_caskroom.exists() {
                    zap_errors.push(format!(
                        "Failed to remove Caskroom version directory during zap: {}",
                        cask_version_path_in_caskroom.display()
                    ));
                }
            }
            if let Some(parent_token_dir) = cask_version_path_in_caskroom.parent() {
                if parent_token_dir.exists() && parent_token_dir.is_dir() {
                    match fs::read_dir(parent_token_dir) {
                        Ok(mut entries) => {
                            if entries.next().is_none()
                                && !remove_filesystem_artifact(parent_token_dir, true)
                                && parent_token_dir.exists()
                            {
                                warn!("Failed to remove empty Caskroom token directory during zap: {}", parent_token_dir.display());
                            }
                        }
                        Err(e) => warn!(
                            "Failed to read Caskroom token dir {} during zap: {}",
                            parent_token_dir.display(),
                            e
                        ),
                    }
                }
            }
            return if zap_errors.is_empty() {
                Ok(())
            } else {
                Err(SpsError::Generic(zap_errors.join("; ")))
            };
        }
    };

    for stanza_map in zap_stanzas {
        for (action_key, action_detail) in &stanza_map.0 {
            debug!(
                "Processing zap action: {} = {:?}",
                action_key, action_detail
            );
            match action_detail {
                ZapActionDetail::Trash(paths) => {
                    for path_str in paths {
                        let target = expand_tilde(path_str, &home);
                        if is_safe_path(&target, &home, config) {
                            if !trash_path(&target) {
                                // Logged within trash_path
                            }
                        } else {
                            zap_errors
                                .push(format!("Skipped unsafe trash path {}", target.display()));
                        }
                    }
                }
                ZapActionDetail::Delete(paths) | ZapActionDetail::Rmdir(paths) => {
                    for path_str in paths {
                        let target = expand_tilde(path_str, &home);
                        if is_safe_path(&target, &home, config) {
                            let use_sudo = target.starts_with("/Library")
                                || target.starts_with("/Applications");
                            let exists_before =
                                target.exists() || target.symlink_metadata().is_ok();
                            if exists_before {
                                if action_key == "rmdir" && !target.is_dir() {
                                    warn!("Zap rmdir target is not a directory: {}. Attempting as file delete.", target.display());
                                }
                                if !remove_filesystem_artifact(&target, use_sudo)
                                    && (target.exists() || target.symlink_metadata().is_ok())
                                {
                                    zap_errors.push(format!(
                                        "Failed to {} {}",
                                        action_key,
                                        target.display()
                                    ));
                                }
                            } else {
                                debug!(
                                    "Zap target {} not found, skipping removal.",
                                    target.display()
                                );
                            }
                        } else {
                            zap_errors.push(format!(
                                "Skipped unsafe {} path {}",
                                action_key,
                                target.display()
                            ));
                        }
                    }
                }
                ZapActionDetail::Pkgutil(ids_sv) => {
                    for id in ids_sv.clone().into_vec() {
                        if !VALID_PKGID_RE.is_match(&id) {
                            warn!("Invalid pkgutil ID format for zap: '{}'. Skipping.", id);
                            zap_errors.push(format!("Invalid pkgutil ID: {id}"));
                            continue;
                        }
                        if !forget_pkgutil_receipt(&id) {
                            // Error logged in helper
                        }
                    }
                }
                ZapActionDetail::Launchctl(labels_sv) => {
                    for label in labels_sv.clone().into_vec() {
                        if !VALID_LABEL_RE.is_match(&label) {
                            warn!(
                                "Invalid launchctl label format for zap: '{}'. Skipping.",
                                label
                            );
                            zap_errors.push(format!("Invalid launchctl label: {label}"));
                            continue;
                        }
                        let potential_paths = vec![
                            home.join("Library/LaunchAgents")
                                .join(format!("{label}.plist")),
                            PathBuf::from("/Library/LaunchAgents").join(format!("{label}.plist")),
                            PathBuf::from("/Library/LaunchDaemons").join(format!("{label}.plist")),
                        ];
                        let path_to_try = potential_paths.into_iter().find(|p| p.exists());
                        if !unload_and_remove_launchd(&label, path_to_try.as_deref()) {
                            // Error logged in helper
                        }
                    }
                }
                ZapActionDetail::Script { executable, args } => {
                    let script_path_str = executable;
                    if !VALID_SCRIPT_PATH_RE.is_match(script_path_str) {
                        error!(
                            "Zap script path contains invalid characters: '{}'. Skipping.",
                            script_path_str
                        );
                        zap_errors.push(format!("Skipped invalid script path: {script_path_str}"));
                        continue;
                    }
                    let script_full_path = PathBuf::from(script_path_str);
                    if !script_full_path.exists() {
                        if !script_full_path.is_absolute() {
                            if let Ok(found_path) = which::which(&script_full_path) {
                                debug!(
                                    "Found zap script {} in PATH: {}",
                                    script_full_path.display(),
                                    found_path.display()
                                );
                                run_zap_script(
                                    &found_path,
                                    args.as_ref().map(|v| v.as_slice()),
                                    &mut zap_errors,
                                );
                            } else {
                                error!(
                                    "Zap script '{}' not found (absolute or in PATH). Skipping.",
                                    script_full_path.display()
                                );
                                zap_errors.push(format!(
                                    "Zap script not found: {}",
                                    script_full_path.display()
                                ));
                            }
                        } else {
                            error!(
                                "Absolute zap script path '{}' not found. Skipping.",
                                script_full_path.display()
                            );
                            zap_errors.push(format!(
                                "Zap script not found: {}",
                                script_full_path.display()
                            ));
                        }
                        continue;
                    }
                    run_zap_script(
                        &script_full_path,
                        args.as_ref().map(|v| v.as_slice()),
                        &mut zap_errors,
                    );
                }
                ZapActionDetail::Signal(signals) => {
                    for signal_spec in signals {
                        let parts: Vec<&str> = signal_spec.splitn(2, '/').collect();
                        if parts.len() != 2 {
                            warn!("Invalid signal spec format '{}', expected SIGNAL/bundle.id. Skipping.", signal_spec);
                            zap_errors.push(format!("Invalid signal spec: {signal_spec}"));
                            continue;
                        }
                        let signal = parts[0].trim().to_uppercase();
                        let bundle_id_or_pattern = parts[1].trim();

                        if !VALID_SIGNAL_RE.is_match(&signal) {
                            warn!(
                                "Invalid signal name '{}' in spec '{}'. Skipping.",
                                signal, signal_spec
                            );
                            zap_errors.push(format!("Invalid signal name: {signal}"));
                            continue;
                        }

                        debug!("Sending signal {} to processes matching ID/pattern '{}' (using pkill -f)", signal, bundle_id_or_pattern);
                        let mut cmd = Command::new("pkill");
                        cmd.arg(format!("-{signal}")); // Standard signal format for pkill
                        cmd.arg("-f");
                        cmd.arg(bundle_id_or_pattern);
                        cmd.stdout(Stdio::null()).stderr(Stdio::piped());
                        match cmd.status() {
                            Ok(status) => {
                                if status.success() {
                                    debug!("Successfully sent signal {} via pkill to processes matching '{}'.", signal, bundle_id_or_pattern);
                                } else if status.code() == Some(1) {
                                    debug!("No running processes found matching ID/pattern '{}' for signal {} via pkill.", bundle_id_or_pattern, signal);
                                } else {
                                    warn!("pkill command failed for signal {} / ID/pattern '{}' with status: {}", signal, bundle_id_or_pattern, status);
                                }
                            }
                            Err(e) => {
                                error!(
                                    "Failed to execute pkill for signal {} / ID/pattern '{}': {}",
                                    signal, bundle_id_or_pattern, e
                                );
                                zap_errors.push(format!("Failed to run pkill for signal {signal}"));
                            }
                        }
                    }
                }
            }
        }
    }

    debug!(
        "Zap: Removing Caskroom version directory: {}",
        cask_version_path_in_caskroom.display()
    );
    if !remove_filesystem_artifact(cask_version_path_in_caskroom, true)
        && cask_version_path_in_caskroom.exists()
    {
        let msg = format!(
            "Failed to remove Caskroom version directory during zap: {}",
            cask_version_path_in_caskroom.display()
        );
        error!("{}", msg);
        zap_errors.push(msg);
    }

    if let Some(parent_token_dir) = cask_version_path_in_caskroom.parent() {
        if parent_token_dir.exists() && parent_token_dir.is_dir() {
            match fs::read_dir(parent_token_dir) {
                Ok(mut entries) => {
                    if entries.next().is_none() {
                        debug!(
                            "Zap: Removing empty Caskroom token directory: {}",
                            parent_token_dir.display()
                        );
                        if !remove_filesystem_artifact(parent_token_dir, true)
                            && parent_token_dir.exists()
                        {
                            warn!(
                                "Failed to remove empty Caskroom token directory during zap: {}",
                                parent_token_dir.display()
                            );
                        }
                    }
                }
                Err(e) => warn!(
                    "Failed to read Caskroom token directory {} during zap cleanup: {}",
                    parent_token_dir.display(),
                    e
                ),
            }
        }
    }

    if zap_errors.is_empty() {
        debug!(
            "Zap process completed successfully for cask: {}",
            cask_def.token
        );
        Ok(())
    } else {
        error!(
            "Zap process for {} completed with errors: {}",
            cask_def.token,
            zap_errors.join("; ")
        );
        Err(SpsError::InstallError(format!(
            "Zap for {} failed with errors: {}",
            cask_def.token,
            zap_errors.join("; ")
        )))
    }
}

fn process_artifact_uninstall_core(
    artifact: &InstalledArtifact,
    config: &Config,
    use_sudo_for_zap: bool,
) -> bool {
    debug!("Processing artifact removal: {:?}", artifact);
    match artifact {
        InstalledArtifact::AppBundle { path } => {
            debug!("Uninstall: Removing AppBundle at {}", path.display());
            match path.symlink_metadata() {
                Ok(metadata) if metadata.file_type().is_symlink() => {
                    debug!("AppBundle at {} is a symlink; unlinking.", path.display());
                    match std::fs::remove_file(path) {
                        Ok(_) => true,
                        Err(e) => {
                            warn!("Failed to unlink symlink at {}: {}", path.display(), e);
                            false
                        }
                    }
                }
                Ok(metadata) if metadata.file_type().is_dir() => {
                    #[cfg(target_os = "macos")]
                    {
                        if path.exists() {
                            if let Err(e) = applescript::quit_app_gracefully(path) {
                                warn!(
                                    "Attempt to gracefully quit app at {} failed: {} (proceeding)",
                                    path.display(),
                                    e
                                );
                            }
                        } else {
                            debug!(
                                "App bundle at {} does not exist, skipping quit.",
                                path.display()
                            );
                        }
                    }
                    let use_sudo = path.starts_with(config.applications_dir())
                        || path.starts_with("/Applications")
                        || use_sudo_for_zap;
                    remove_filesystem_artifact(path, use_sudo)
                }
                Ok(_) | Err(_) => {
                    let use_sudo = path.starts_with(config.applications_dir())
                        || path.starts_with("/Applications")
                        || use_sudo_for_zap;
                    remove_filesystem_artifact(path, use_sudo)
                }
            }
        }
        InstalledArtifact::BinaryLink { link_path, .. }
        | InstalledArtifact::ManpageLink { link_path, .. }
        | InstalledArtifact::CaskroomLink { link_path, .. } => {
            debug!("Uninstall: Removing link at {}", link_path.display());
            remove_filesystem_artifact(link_path, use_sudo_for_zap)
        }
        InstalledArtifact::PkgUtilReceipt { id } => {
            debug!("Uninstall: Forgetting PkgUtilReceipt {}", id);
            forget_pkgutil_receipt(id)
        }
        InstalledArtifact::Launchd { label, path } => {
            debug!("Uninstall: Unloading Launchd {} (path: {:?})", label, path);
            unload_and_remove_launchd(label, path.as_deref())
        }
        InstalledArtifact::MovedResource { path } => {
            debug!("Uninstall: Removing MovedResource at {}", path.display());
            remove_filesystem_artifact(path, use_sudo_for_zap)
        }
        InstalledArtifact::CaskroomReference { path } => {
            debug!(
                "Uninstall: Removing CaskroomReference at {}",
                path.display()
            );
            remove_filesystem_artifact(path, use_sudo_for_zap)
        }
    }
}

fn forget_pkgutil_receipt(id: &str) -> bool {
    if !VALID_PKGID_RE.is_match(id) {
        error!("Invalid pkgutil ID format: '{}'. Skipping forget.", id);
        return false;
    }
    debug!("Forgetting package receipt (requires sudo): {}", id);
    let output = Command::new("sudo")
        .arg("pkgutil")
        .arg("--forget")
        .arg(id)
        .output();
    match output {
        Ok(out) if out.status.success() => {
            debug!("Successfully forgot package receipt {}", id);
            true
        }
        Ok(out) => {
            let stderr = String::from_utf8_lossy(&out.stderr);
            if !stderr.contains("No receipt for") && !stderr.trim().is_empty() {
                error!("Failed to forget package receipt {}: {}", id, stderr.trim());
                // Return false if pkgutil fails for a reason other than "No receipt"
                return false;
            } else {
                debug!("Package receipt {} already forgotten or never existed.", id);
            }
            true
        }
        Err(e) => {
            error!("Failed to execute sudo pkgutil --forget {}: {}", id, e);
            false
        }
    }
}

fn unload_and_remove_launchd(label: &str, path: Option<&Path>) -> bool {
    if !VALID_LABEL_RE.is_match(label) {
        error!(
            "Invalid launchd label format: '{}'. Skipping unload/remove.",
            label
        );
        return false;
    }
    debug!("Unloading launchd service (if loaded): {}", label);
    let unload_output = Command::new("launchctl")
        .arg("unload")
        .arg("-w")
        .arg(label)
        .stderr(Stdio::piped())
        .output();

    let mut unload_successful_or_not_loaded = false;
    match unload_output {
        Ok(out) if out.status.success() => {
            debug!("Successfully unloaded launchd service {}", label);
            unload_successful_or_not_loaded = true;
        }
        Ok(out) => {
            let stderr = String::from_utf8_lossy(&out.stderr);
            if stderr.contains("Could not find specified service")
                || stderr.contains("service is not loaded")
                || stderr.trim().is_empty()
            {
                debug!("Launchd service {} already unloaded or not found.", label);
                unload_successful_or_not_loaded = true;
            } else {
                warn!(
                    "launchctl unload {} failed (but proceeding with plist removal attempt): {}",
                    label,
                    stderr.trim()
                );
                // Proceed to try plist removal even if unload reports other errors
            }
        }
        Err(e) => {
            warn!("Failed to execute launchctl unload {} (but proceeding with plist removal attempt): {}", label, e);
        }
    }

    if let Some(plist_path) = path {
        if plist_path.exists() {
            debug!("Removing launchd plist file: {}", plist_path.display());
            let use_sudo = plist_path.starts_with("/Library/LaunchDaemons")
                || plist_path.starts_with("/Library/LaunchAgents");
            if !remove_filesystem_artifact(plist_path, use_sudo) {
                warn!("Failed to remove launchd plist: {}", plist_path.display());
                return false; // If plist removal fails, consider the operation failed
            }
        } else {
            debug!(
                "Launchd plist path {} does not exist, skip removal.",
                plist_path.display()
            );
        }
    } else {
        debug!(
            "No path provided for launchd plist removal for label {}",
            label
        );
    }
    unload_successful_or_not_loaded // Success depends on unload and optional plist removal
}

fn trash_path(path: &Path) -> bool {
    if !path.exists() && path.symlink_metadata().is_err() {
        debug!("Path for trashing not found: {}", path.display());
        return true;
    }
    match trash::delete(path) {
        Ok(_) => {
            debug!("Trashed: {}", path.display());
            true
        }
        Err(e) => {
            warn!("Failed to trash {} (proceeding anyway): {}. This might require manual cleanup or installing a 'trash' utility.", path.display(), e);
            true
        }
    }
}

/// Helper for zap scripts.
fn run_zap_script(script_path: &Path, args: Option<&[String]>, errors: &mut Vec<String>) {
    debug!(
        "Running zap script: {} with args {:?}",
        script_path.display(),
        args.unwrap_or_default()
    );
    let mut cmd = Command::new(script_path);
    if let Some(script_args) = args {
        cmd.args(script_args);
    }
    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());

    match cmd.output() {
        Ok(output) => {
            log_command_output(
                "Zap script",
                &script_path.display().to_string(),
                &output,
                errors,
            );
        }
        Err(e) => {
            let msg = format!(
                "Failed to execute zap script '{}': {}",
                script_path.display(),
                e
            );
            error!("{}", msg);
            errors.push(msg);
        }
    }
}

/// Logs the output of a command and adds to error list if it failed.
fn log_command_output(
    context: &str,
    command_str: &str,
    output: &std::process::Output,
    errors: &mut Vec<String>,
) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    if !output.status.success() {
        error!(
            "{} '{}' failed with status {}: {}",
            context,
            command_str,
            output.status,
            stderr.trim()
        );
        if !stdout.trim().is_empty() {
            error!("{} stdout: {}", context, stdout.trim());
        }
        errors.push(format!("{context} '{command_str}' failed"));
    } else {
        debug!("{} '{}' executed successfully.", context, command_str);
        if !stdout.trim().is_empty() {
            debug!("{} stdout: {}", context, stdout.trim());
        }
        if !stderr.trim().is_empty() {
            debug!("{} stderr: {}", context, stderr.trim());
        }
    }
}

// Helper function specifically for cleaning up the private store.
// This was originally inside zap_cask_artifacts.
fn cleanup_private_store(
    cask_token: &str,
    version: &str,
    app_name: Option<&str>, // The actual .app name, not the token
    config: &Config,
) -> bool {
    debug!(
        "Cleaning up private store for cask {} version {}",
        cask_token, version
    );

    let private_version_dir = config.cask_store_version_path(cask_token, version);

    if let Some(app) = app_name {
        let app_path_in_private_store = private_version_dir.join(app);
        if app_path_in_private_store.exists()
            || app_path_in_private_store.symlink_metadata().is_ok()
        {
            debug!(
                "Removing app from private store: {}",
                app_path_in_private_store.display()
            );
            // Use the helper from install::cask::helpers, assuming it's correctly located and
            // public
            if !remove_path_robustly_from_install_helpers(&app_path_in_private_store, config, false)
            {
                // use_sudo=false for private store
                warn!(
                    "Failed to remove app from private store: {}",
                    app_path_in_private_store.display()
                );
                // Potentially return false or collect errors, depending on desired strictness
            }
        }
    }

    // After attempting to remove specific app, remove the version directory if it exists
    // This also handles cases where app_name was None.
    if private_version_dir.exists() {
        debug!(
            "Removing private store version directory: {}",
            private_version_dir.display()
        );
        match fs::remove_dir_all(&private_version_dir) {
            Ok(_) => debug!(
                "Successfully removed private store version directory {}",
                private_version_dir.display()
            ),
            Err(e) => {
                warn!(
                    "Failed to remove private store version directory {}: {}",
                    private_version_dir.display(),
                    e
                );
                return false; // If the version dir removal fails, consider it a failure
            }
        }
    }

    // Clean up empty parent token directory.
    cleanup_empty_parent_dirs_in_private_store(
        &private_version_dir, // Start from the version dir (or its parent if it was just removed)
        &config.cask_store_dir(),
    );

    true
}

// In sps-core/src/build/cask/app.rs

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;
use tracing::{debug, error, warn};

#[cfg(target_os = "macos")]
/// Finds the primary .app bundle in a directory. Returns an error if none or ambiguous.
/// If multiple .app bundles are found, returns the first and logs a warning.
pub fn find_primary_app_bundle_in_dir(dir: &Path) -> Result<PathBuf> {
    if !dir.is_dir() {
        return Err(SpsError::NotFound(format!(
            "Directory {} not found for app bundle scan.",
            dir.display()
        )));
    }
    let mut app_bundles = Vec::new();
    for entry_res in fs::read_dir(dir)? {
        let entry = entry_res?;
        let path = entry.path();
        if path.is_dir() && path.extension().is_some_and(|ext| ext == "app") {
            app_bundles.push(path);
        }
    }
    if app_bundles.is_empty() {
        Err(SpsError::NotFound(format!(
            "No .app bundle found in {}",
            dir.display()
        )))
    } else if app_bundles.len() == 1 {
        Ok(app_bundles.remove(0))
    } else {
        // Heuristic: return the largest .app bundle if multiple are found, or one matching a common
        // pattern. For now, error if multiple are present to force explicit handling in
        // Cask definitions if needed.
        warn!("Multiple .app bundles found in {}: {:?}. Returning the first one, but this might be ambiguous.", dir.display(), app_bundles);
        Ok(app_bundles.remove(0)) // Or error out
    }
}

use sps_common::pipeline::JobAction;

pub fn install_app_from_staged(
    cask: &Cask,
    staged_app_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
    job_action: &JobAction,
) -> Result<Vec<InstalledArtifact>> {
    if !staged_app_path.exists() || !staged_app_path.is_dir() {
        return Err(SpsError::NotFound(format!(
            "Staged app bundle for {} not found or is not a directory: {}",
            cask.token,
            staged_app_path.display()
        )));
    }

    let app_name = staged_app_path
        .file_name()
        .ok_or_else(|| {
            SpsError::Generic(format!(
                "Invalid staged app path (no filename): {}",
                staged_app_path.display()
            ))
        })?
        .to_string_lossy();

    let new_version_str = cask.version.clone().unwrap_or_else(|| "latest".to_string());
    let final_private_store_app_path: PathBuf;

    // Determine if we are upgrading and if the old private store app path exists
    let mut did_upgrade = false;

    if let JobAction::Upgrade {
        from_version,
        old_install_path,
        ..
    } = job_action
    {
        debug!(
            "[{}] Processing app install as UPGRADE from version {}",
            cask.token, from_version
        );

        // Try to get primary_app_file_name from the old manifest to build the old private store
        // path
        let old_manifest_path = old_install_path.join("CASK_INSTALL_MANIFEST.json");
        let old_primary_app_name = if old_manifest_path.is_file() {
            fs::read_to_string(&old_manifest_path)
                .ok()
                .and_then(|s| {
                    serde_json::from_str::<crate::install::cask::CaskInstallManifest>(&s).ok()
                })
                .and_then(|m| m.primary_app_file_name)
        } else {
            // Fallback if old manifest is missing, use current app_name (less reliable if app name
            // changed)
            warn!("[{}] Old manifest not found at {} during upgrade. Using current app name '{}' for private store path derivation.", cask.token, old_manifest_path.display(), app_name);
            Some(app_name.to_string())
        };

        if let Some(name_for_old_path) = old_primary_app_name {
            let old_private_store_app_dir_path =
                config.cask_store_version_path(&cask.token, from_version);
            let old_private_store_app_bundle_path =
                old_private_store_app_dir_path.join(&name_for_old_path);
            if old_private_store_app_bundle_path.exists()
                && old_private_store_app_bundle_path.is_dir()
            {
                debug!("[{}] UPGRADE: Old private store app bundle found at {}. Using Homebrew-style overwrite strategy.", cask.token, old_private_store_app_bundle_path.display());

                // ========================================================================
                // CRITICAL: Homebrew-Style App Bundle Replacement Strategy
                // ========================================================================
                // WHY THIS APPROACH:
                // 1. Preserves extended attributes (quarantine, code signing, etc.)
                // 2. Maintains app bundle identity → prevents Gatekeeper reset
                // 3. Avoids breaking symlinks and file system references
                // 4. Ensures user data in ~/Library remains accessible to the app
                //
                // DO NOT CHANGE TO fs::rename() or similar - it breaks Gatekeeper!
                // ========================================================================

                // Step 1: Remove the old app bundle from private store
                // (This is safe because we're about to replace it with the new version)
                let rm_status = Command::new("rm")
                    .arg("-rf")
                    .arg(&old_private_store_app_bundle_path)
                    .status()
                    .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

                if !rm_status.success() {
                    return Err(SpsError::InstallError(format!(
                        "Failed to remove old app bundle during upgrade: {}",
                        old_private_store_app_bundle_path.display()
                    )));
                }

                // Step 2: Copy the new app bundle with ALL attributes preserved
                // The -pR flags are critical:
                // -p: Preserve file attributes, ownership, timestamps
                // -R: Recursive copy for directories
                // This ensures Gatekeeper approval and code signing are maintained
                let cp_status = Command::new("cp")
                    .arg("-pR") // CRITICAL: Preserve all attributes, links, and metadata
                    .arg(staged_app_path)
                    .arg(&old_private_store_app_bundle_path)
                    .status()
                    .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

                if !cp_status.success() {
                    return Err(SpsError::InstallError(format!(
                        "Failed to copy new app bundle during upgrade: {} -> {}",
                        staged_app_path.display(),
                        old_private_store_app_bundle_path.display()
                    )));
                }

                debug!(
                    "[{}] UPGRADE: Successfully overwrote old app bundle with new version using cp -pR",
                    cask.token
                );

                // Now, rename the parent version directory (e.g., .../1.0 -> .../1.1)
                let new_private_store_version_dir =
                    config.cask_store_version_path(&cask.token, &new_version_str);
                if old_private_store_app_dir_path != new_private_store_version_dir {
                    debug!(
                        "[{}] Renaming private store version dir from {} to {}",
                        cask.token,
                        old_private_store_app_dir_path.display(),
                        new_private_store_version_dir.display()
                    );
                    fs::rename(
                        &old_private_store_app_dir_path,
                        &new_private_store_version_dir,
                    )
                    .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
                }
                final_private_store_app_path =
                    new_private_store_version_dir.join(app_name.as_ref());
                did_upgrade = true;
            } else {
                warn!("[{}] UPGRADE: Old private store app path {} not found or not a dir. Proceeding with fresh private store placement for new version.", cask.token, old_private_store_app_bundle_path.display());
                // Fallback to fresh placement
                final_private_store_app_path =
                    config.cask_store_app_path(&cask.token, &new_version_str, app_name.as_ref());
            }
        } else {
            warn!("[{}] UPGRADE: Could not determine old app bundle name. Proceeding with fresh private store placement for new version.", cask.token);
            final_private_store_app_path =
                config.cask_store_app_path(&cask.token, &new_version_str, app_name.as_ref());
        }
    } else {
        // Not an upgrade
        final_private_store_app_path =
            config.cask_store_app_path(&cask.token, &new_version_str, app_name.as_ref());
    }

    let final_app_destination_in_applications = config.applications_dir().join(app_name.as_ref());
    let caskroom_symlink_to_final_app = cask_version_install_path.join(app_name.as_ref());

    debug!(
        "Installing app '{}': Staged -> Private Store -> /Applications -> Caskroom Symlink",
        app_name
    );
    debug!("  Staged app source: {}", staged_app_path.display());
    debug!(
        "  Private store copy target: {}",
        final_private_store_app_path.display()
    );
    debug!(
        "  Final /Applications target: {}",
        final_app_destination_in_applications.display()
    );
    debug!(
        "  Caskroom symlink target: {}",
        caskroom_symlink_to_final_app.display()
    );

    // 1. Ensure Caskroom version path exists
    if !cask_version_install_path.exists() {
        fs::create_dir_all(cask_version_install_path).map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!(
                    "Failed to create cask version dir {}: {}",
                    cask_version_install_path.display(),
                    e
                ),
            )))
        })?;
    }

    // 2. Create private store directory if it doesn't exist
    if let Some(parent) = final_private_store_app_path.parent() {
        if !parent.exists() {
            debug!("Creating private store directory: {}", parent.display());
            fs::create_dir_all(parent).map_err(|e| {
                SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                    e.kind(),
                    format!(
                        "Failed to create private store dir {}: {}",
                        parent.display(),
                        e
                    ),
                )))
            })?;
        }
    }

    if !did_upgrade {
        // 3. Clean existing app in private store (if any from a failed prior attempt)
        if final_private_store_app_path.exists()
            || final_private_store_app_path.symlink_metadata().is_ok()
        {
            debug!(
                "Removing existing item at private store path: {}",
                final_private_store_app_path.display()
            );
            let _ = remove_path_robustly(&final_private_store_app_path, config, false);
        }

        // 4. Move from temporary stage to private store
        debug!(
            "Moving staged app {} to private store path {}",
            staged_app_path.display(),
            final_private_store_app_path.display()
        );
        if let Err(e) = fs::rename(staged_app_path, &final_private_store_app_path) {
            error!(
                "Failed to move staged app to private store: {}. Source: {}, Dest: {}",
                e,
                staged_app_path.display(),
                final_private_store_app_path.display()
            );
            return Err(SpsError::Io(std::sync::Arc::new(e)));
        }
    }

    // 5. Set/Verify Quarantine on private store copy (only if not already present)
    #[cfg(target_os = "macos")]
    {
        debug!(
            "Setting/verifying quarantine on private store copy: {}",
            final_private_store_app_path.display()
        );
        if let Err(e) = crate::utils::xattr::ensure_quarantine_attribute(
            &final_private_store_app_path,
            &cask.token,
        ) {
            error!(
                "Failed to set quarantine on private store copy {}: {}. This is critical.",
                final_private_store_app_path.display(),
                e
            );
            return Err(SpsError::InstallError(format!(
                "Failed to set quarantine on private store copy {}: {}",
                final_private_store_app_path.display(),
                e
            )));
        }
    }

    // ============================================================================
    // STEP 6: Handle /Applications Destination - THE MOST CRITICAL PART
    // ============================================================================
    // This is where we apply Homebrew's breakthrough strategy that preserves
    // user data and prevents Gatekeeper resets during upgrades.
    //
    // UPGRADE vs FRESH INSTALL Strategy:
    // - UPGRADE: Overwrite app in /Applications directly (preserves identity)
    // - FRESH INSTALL: Use symlink approach (normal installation)
    //
    // WHY THIS SPLIT MATTERS:
    // During upgrades, the app in /Applications already has:
    // 1. Gatekeeper approval and quarantine exemptions
    // 2. Extended attributes that macOS recognizes
    // 3. User trust and security context
    // 4. Associated user data in ~/Library that the app can access
    //
    // By overwriting IN PLACE with cp -pR, we maintain all of this state.
    // ============================================================================

    if let JobAction::Upgrade { .. } = job_action {
        // =======================================================================
        // UPGRADE PATH: Direct Overwrite Strategy (Homebrew's Approach)
        // =======================================================================
        // This is the breakthrough that prevents Gatekeeper resets and data loss.
        // We overwrite the existing app directly rather than removing and
        // re-symlinking, which would break the app's established identity.
        // =======================================================================

        if final_app_destination_in_applications.exists() {
            debug!(
                "UPGRADE: Overwriting existing app at /Applications using Homebrew strategy: {}",
                final_app_destination_in_applications.display()
            );

            // Step 1: Remove the old app in /Applications
            // We need sudo because /Applications requires elevated permissions
            let rm_status = Command::new("sudo")
                .arg("rm")
                .arg("-rf")
                .arg(&final_app_destination_in_applications)
                .status()
                .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

            if !rm_status.success() {
                return Err(SpsError::InstallError(format!(
                    "Failed to remove existing app during upgrade: {}",
                    final_app_destination_in_applications.display()
                )));
            }

            // Copy the new app directly to /Applications, preserving all attributes
            let cp_status = Command::new("sudo")
                .arg("cp")
                .arg("-pR") // Preserve all attributes, links, and metadata
                .arg(&final_private_store_app_path)
                .arg(&final_app_destination_in_applications)
                .status()
                .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

            if !cp_status.success() {
                return Err(SpsError::InstallError(format!(
                    "Failed to copy new app to /Applications during upgrade: {}",
                    final_app_destination_in_applications.display()
                )));
            }

            debug!(
                "UPGRADE: Successfully overwrote app in /Applications, preserving identity: {}",
                final_app_destination_in_applications.display()
            );
        } else {
            // App doesn't exist in /Applications during upgrade - fall back to symlink approach
            debug!(
                "UPGRADE: App not found in /Applications, creating fresh symlink: {}",
                final_app_destination_in_applications.display()
            );
            if let Err(e) = std::os::unix::fs::symlink(
                &final_private_store_app_path,
                &final_app_destination_in_applications,
            ) {
                error!(
                    "Failed to symlink app from private store to /Applications: {} -> {}: {}",
                    final_private_store_app_path.display(),
                    final_app_destination_in_applications.display(),
                    e
                );
                return Err(SpsError::InstallError(format!(
                    "Failed to symlink app from private store to {}: {}",
                    final_app_destination_in_applications.display(),
                    e
                )));
            }
        }
    } else {
        // Fresh install: Clean existing app destination and create symlink
        if final_app_destination_in_applications.exists()
            || final_app_destination_in_applications
                .symlink_metadata()
                .is_ok()
        {
            debug!(
                "Removing existing app at /Applications: {}",
                final_app_destination_in_applications.display()
            );
            if !remove_path_robustly(&final_app_destination_in_applications, config, true) {
                return Err(SpsError::InstallError(format!(
                    "Failed to remove existing app at {}",
                    final_app_destination_in_applications.display()
                )));
            }
        }

        // 7. Symlink from /Applications to private store app bundle
        debug!(
            "INFO: About to symlink app from private store {} to /Applications {}",
            final_private_store_app_path.display(),
            final_app_destination_in_applications.display()
        );
        if let Err(e) = std::os::unix::fs::symlink(
            &final_private_store_app_path,
            &final_app_destination_in_applications,
        ) {
            error!(
                "Failed to symlink app from private store to /Applications: {} -> {}: {}",
                final_private_store_app_path.display(),
                final_app_destination_in_applications.display(),
                e
            );
            return Err(SpsError::InstallError(format!(
                "Failed to symlink app from private store to {}: {}",
                final_app_destination_in_applications.display(),
                e
            )));
        }
    }

    // Remove quarantine attributes from the app in /Applications (whether copied or symlinked)
    #[cfg(target_os = "macos")]
    {
        use xattr;
        let _ = xattr::remove(
            &final_app_destination_in_applications,
            "com.apple.quarantine",
        );
        let _ = xattr::remove(
            &final_app_destination_in_applications,
            "com.apple.provenance",
        );
        let _ = xattr::remove(&final_app_destination_in_applications, "com.apple.macl");
    }

    match job_action {
        JobAction::Upgrade { .. } => {
            debug!(
                "INFO: Successfully updated app in /Applications during upgrade: {}",
                final_app_destination_in_applications.display()
            );
        }
        _ => {
            debug!(
                "INFO: Successfully symlinked app from private store {} to /Applications {}",
                final_private_store_app_path.display(),
                final_app_destination_in_applications.display()
            );
        }
    }

    // 7. No quarantine set on the symlink in /Applications; attribute remains on private store
    //    copy.

    // 8. Create Caskroom Symlink TO the app in /Applications
    let actual_caskroom_symlink_path = cask_version_install_path.join(app_name.as_ref());
    debug!(
        "Creating Caskroom symlink {} -> {}",
        actual_caskroom_symlink_path.display(),
        final_app_destination_in_applications.display()
    );

    if actual_caskroom_symlink_path.symlink_metadata().is_ok() {
        if let Err(e) = fs::remove_file(&actual_caskroom_symlink_path) {
            warn!(
                "Failed to remove existing item at Caskroom symlink path {}: {}. Proceeding.",
                actual_caskroom_symlink_path.display(),
                e
            );
        }
    }

    #[cfg(unix)]
    {
        if let Err(e) = std::os::unix::fs::symlink(
            &final_app_destination_in_applications,
            &actual_caskroom_symlink_path,
        ) {
            error!(
                "Failed to create Caskroom symlink {} -> {}: {}",
                actual_caskroom_symlink_path.display(),
                final_app_destination_in_applications.display(),
                e
            );
            let _ = remove_path_robustly(&final_app_destination_in_applications, config, true);
            return Err(SpsError::Io(std::sync::Arc::new(e)));
        }
    }

    let mut created_artifacts = vec![InstalledArtifact::AppBundle {
        path: final_app_destination_in_applications.clone(),
    }];
    created_artifacts.push(InstalledArtifact::CaskroomLink {
        link_path: actual_caskroom_symlink_path,
        target_path: final_app_destination_in_applications.clone(),
    });

    debug!(
        "Successfully installed app artifact: {} (Cask: {})",
        app_name, cask.token
    );

    // Write CASK_INSTALL_MANIFEST.json to ensure package is always detected as installed
    if let Err(e) = crate::install::cask::write_cask_manifest(
        cask,
        cask_version_install_path,
        created_artifacts.clone(),
    ) {
        error!(
            "Failed to write CASK_INSTALL_MANIFEST.json for {}: {}",
            cask.token, e
        );
        return Err(SpsError::InstallError(format!(
            "Failed to write CASK_INSTALL_MANIFEST.json for {}: {}",
            cask.token, e
        )));
    }

    Ok(created_artifacts)
}

/// Helper function for robust path removal (internal to app.rs or moved to a common util)
fn remove_path_robustly(path: &Path, _config: &Config, use_sudo_if_needed: bool) -> bool {
    if !path.exists() && path.symlink_metadata().is_err() {
        debug!("Path {} not found for removal.", path.display());
        return true;
    }
    let is_dir = path.is_dir()
        && !path
            .symlink_metadata()
            .is_ok_and(|m| m.file_type().is_symlink());
    let removal_op = || -> std::io::Result<()> {
        if is_dir {
            fs::remove_dir_all(path)
        } else {
            fs::remove_file(path)
        }
    };

    if let Err(e) = removal_op() {
        if e.kind() == std::io::ErrorKind::NotFound {
            return true;
        }
        if use_sudo_if_needed && e.kind() == std::io::ErrorKind::PermissionDenied {
            warn!(
                "Direct removal of {} failed (Permission Denied). Trying with sudo rm -rf.",
                path.display()
            );
            let output = Command::new("sudo").arg("rm").arg("-rf").arg(path).output();
            match output {
                Ok(out) if out.status.success() => {
                    debug!("Successfully removed {} with sudo.", path.display());
                    return true;
                }
                Ok(out) => {
                    error!(
                        "`sudo rm -rf {}` failed ({}): {}",
                        path.display(),
                        out.status,
                        String::from_utf8_lossy(&out.stderr).trim()
                    );
                    return false;
                }
                Err(sudo_e) => {
                    error!(
                        "Error executing `sudo rm -rf` for {}: {}",
                        path.display(),
                        sudo_e
                    );
                    return false;
                }
            }
        } else {
            error!("Failed to remove {}: {}", path.display(), e);
            return false;
        }
    }
    debug!("Successfully removed {}.", path.display());
    true
}

pub mod artifacts;
pub mod dmg;
pub mod helpers;

use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};

use infer;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::{Cask, Sha256Field, UrlField};
use sps_net::http::ProgressCallback;
use tempfile::TempDir;
use tracing::{debug, error};

use crate::install::extract;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaskInstallManifest {
    pub manifest_format_version: String,
    pub token: String,
    pub version: String,
    pub installed_at: u64,
    pub artifacts: Vec<InstalledArtifact>,
    pub primary_app_file_name: Option<String>,
    pub is_installed: bool,              // New flag for soft uninstall
    pub cask_store_path: Option<String>, // Path to private store app, if available
}

/// Returns the path to the cask's version directory in the private store.
pub fn sps_private_cask_version_dir(cask: &Cask, config: &Config) -> PathBuf {
    let version = cask.version.clone().unwrap_or_else(|| "latest".to_string());
    config.cask_store_version_path(&cask.token, &version)
}

/// Returns the path to the cask's token directory in the private store.
pub fn sps_private_cask_token_dir(cask: &Cask, config: &Config) -> PathBuf {
    config.cask_store_token_path(&cask.token)
}

/// Returns the path to the main app bundle for a cask in the private store.
/// This assumes the primary app bundle is named as specified in the cask's artifacts.
pub fn sps_private_cask_app_path(cask: &Cask, config: &Config) -> Option<PathBuf> {
    let version = cask.version.clone().unwrap_or_else(|| "latest".to_string());
    if let Some(cask_artifacts) = &cask.artifacts {
        for artifact in cask_artifacts {
            if let Some(obj) = artifact.as_object() {
                if let Some(apps) = obj.get("app") {
                    if let Some(app_names) = apps.as_array() {
                        if let Some(app_name_val) = app_names.first() {
                            if let Some(app_name) = app_name_val.as_str() {
                                return Some(config.cask_store_app_path(
                                    &cask.token,
                                    &version,
                                    app_name,
                                ));
                            }
                        }
                    }
                }
            }
        }
    }
    None
}

pub async fn download_cask(cask: &Cask, cache: &Cache) -> Result<PathBuf> {
    download_cask_with_progress(cask, cache, None).await
}

pub async fn download_cask_with_progress(
    cask: &Cask,
    cache: &Cache,
    progress_callback: Option<ProgressCallback>,
) -> Result<PathBuf> {
    let url_field = cask
        .url
        .as_ref()
        .ok_or_else(|| SpsError::Generic(format!("Cask {} has no URL", cask.token)))?;
    let url_str = match url_field {
        UrlField::Simple(u) => u.as_str(),
        UrlField::WithSpec { url, .. } => url.as_str(),
    };

    if url_str.is_empty() {
        return Err(SpsError::Generic(format!(
            "Cask {} has an empty URL",
            cask.token
        )));
    }

    debug!("Downloading cask from {}", url_str);
    let parsed = Url::parse(url_str)
        .map_err(|e| SpsError::Generic(format!("Invalid URL '{url_str}': {e}")))?;
    sps_net::validation::validate_url(parsed.as_str())?;
    let file_name = parsed
        .path_segments()
        .and_then(|mut segments| segments.next_back())
        .filter(|s| !s.is_empty())
        .map(|s| s.to_string())
        .unwrap_or_else(|| {
            debug!("URL has no filename component, using fallback name for cache based on token.");
            format!("cask-{}-download.tmp", cask.token.replace('/', "_"))
        });
    let cache_key = format!("cask-{}-{}", cask.token, file_name);
    let cache_path = cache.get_dir().join("cask_downloads").join(&cache_key);

    if cache_path.exists() {
        debug!("Using cached download: {}", cache_path.display());
        return Ok(cache_path);
    }

    use futures::StreamExt;
    use tokio::fs::File as TokioFile;
    use tokio::io::AsyncWriteExt;

    let client = reqwest::Client::new();
    let response = client
        .get(parsed.clone())
        .send()
        .await
        .map_err(|e| SpsError::Http(std::sync::Arc::new(e)))?;
    if !response.status().is_success() {
        return Err(SpsError::DownloadError(
            cask.token.clone(),
            url_str.to_string(),
            format!("HTTP status {}", response.status()),
        ));
    }

    // Get total size from Content-Length header if available
    let total_size = response.content_length();

    if let Some(parent) = cache_path.parent() {
        fs::create_dir_all(parent)?;
    }

    let mut file = TokioFile::create(&cache_path)
        .await
        .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

    // Use bytes_stream() for chunked download with progress reporting
    let mut stream = response.bytes_stream();
    let mut bytes_downloaded = 0u64;

    while let Some(chunk) = stream.next().await {
        let chunk = chunk.map_err(|e| SpsError::Http(std::sync::Arc::new(e)))?;

        file.write_all(&chunk)
            .await
            .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

        bytes_downloaded += chunk.len() as u64;

        // Call progress callback if provided
        if let Some(ref callback) = progress_callback {
            callback(bytes_downloaded, total_size);
        }
    }

    drop(file);
    match cask.sha256.as_ref() {
        Some(Sha256Field::Hex(s)) => {
            if s.eq_ignore_ascii_case("no_check") {
                tracing::debug!(
                    "Skipping checksum verification for cask {} due to 'no_check' string.",
                    cache_path.display()
                );
            } else if !s.is_empty() {
                match sps_net::validation::verify_checksum(&cache_path, s) {
                    Ok(_) => {
                        tracing::debug!(
                            "Cask download checksum verified: {}",
                            cache_path.display()
                        );
                    }
                    Err(e) => {
                        tracing::error!(
                            "Cask download checksum mismatch ({}). Deleting cached file.",
                            e
                        );
                        let _ = fs::remove_file(&cache_path);
                        return Err(e);
                    }
                }
            } else {
                tracing::warn!(
                    "Skipping checksum verification for cask {} - empty sha256 provided.",
                    cache_path.display()
                );
            }
        }
        Some(Sha256Field::NoCheck { no_check: true }) => {
            tracing::debug!(
                "Skipping checksum verification for cask {} due to 'no_check'.",
                cache_path.display()
            );
        }
        _ => {
            tracing::warn!(
                "Skipping checksum verification for cask {} - none provided.",
                cache_path.display()
            );
        }
    }
    debug!("Download completed: {}", cache_path.display());

    // --- Set quarantine xattr on the downloaded archive (macOS only) ---
    #[cfg(target_os = "macos")]
    {
        if let Err(e) = crate::utils::xattr::set_quarantine_attribute(&cache_path, "sps-downloader")
        {
            tracing::warn!(
                "Failed to set quarantine attribute on downloaded archive {}: {}. Extraction and installation will proceed, but Gatekeeper behavior might be affected.",
                cache_path.display(),
                e
            );
        }
    }

    Ok(cache_path)
}

use sps_common::pipeline::JobAction;

pub fn install_cask(
    cask: &Cask,
    download_path: &Path,
    config: &Config,
    job_action: &JobAction,
) -> Result<()> {
    debug!("Installing cask: {}", cask.token);
    // This is the path in the *actual* Caskroom (e.g., /opt/homebrew/Caskroom/token/version)
    // where metadata and symlinks to /Applications will go.
    let actual_cask_room_version_path = config.cask_room_version_path(
        &cask.token,
        &cask.version.clone().unwrap_or_else(|| "latest".to_string()),
    );

    if !actual_cask_room_version_path.exists() {
        fs::create_dir_all(&actual_cask_room_version_path).map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!(
                    "Failed create cask_room dir {}: {}",
                    actual_cask_room_version_path.display(),
                    e
                ),
            )))
        })?;
        debug!(
            "Created actual cask_room version directory: {}",
            actual_cask_room_version_path.display()
        );
    }
    let mut detected_extension = download_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_lowercase();
    let non_extensions = ["stable", "latest", "download", "bin", ""];
    if non_extensions.contains(&detected_extension.as_str()) {
        debug!(
            "Download path '{}' has no definite extension ('{}'), attempting content detection.",
            download_path.display(),
            detected_extension
        );
        match infer::get_from_path(download_path) {
            Ok(Some(kind)) => {
                detected_extension = kind.extension().to_string();
                debug!("Detected file type via content: {}", detected_extension);
            }
            Ok(None) => {
                error!(
                    "Could not determine file type from content for: {}",
                    download_path.display()
                );
                return Err(SpsError::Generic(format!(
                    "Could not determine file type for download: {}",
                    download_path.display()
                )));
            }
            Err(e) => {
                error!(
                    "Error reading file for type detection {}: {}",
                    download_path.display(),
                    e
                );
                return Err(SpsError::Io(std::sync::Arc::new(e)));
            }
        }
    } else {
        debug!(
            "Using file extension for type detection: {}",
            detected_extension
        );
    }
    if detected_extension == "pkg" || detected_extension == "mpkg" {
        debug!("Detected PKG installer, running directly");
        match artifacts::pkg::install_pkg_from_path(
            cask,
            download_path,
            &actual_cask_room_version_path, // PKG manifest items go into the actual cask_room
            config,
        ) {
            Ok(installed_artifacts) => {
                debug!("Writing PKG install manifest");
                write_cask_manifest(cask, &actual_cask_room_version_path, installed_artifacts)?;
                debug!("Successfully installed PKG cask: {}", cask.token);
                return Ok(());
            }
            Err(e) => {
                debug!("Failed to install PKG: {}", e);
                // Clean up cask_room on error
                let _ = fs::remove_dir_all(&actual_cask_room_version_path);
                return Err(e);
            }
        }
    }
    let stage_dir = TempDir::new().map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed to create staging directory: {e}"),
        )))
    })?;
    let stage_path = stage_dir.path();
    debug!("Created staging directory: {}", stage_path.display());
    // Determine expected extension (this might need refinement)
    // Option 1: Parse from URL
    let expected_ext_from_url = download_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");
    // Option 2: A new field in Cask JSON definition (preferred)
    // let expected_ext = cask.expected_extension.as_deref().unwrap_or(expected_ext_from_url);
    let expected_ext = expected_ext_from_url; // Use URL for now

    if !expected_ext.is_empty()
        && crate::build::compile::RECOGNISED_SINGLE_FILE_EXTENSIONS.contains(&expected_ext)
    {
        // Check if it's an archive/installer type we handle
        tracing::debug!(
            "Verifying content type for {} against expected extension '{}'",
            download_path.display(),
            expected_ext
        );
        if let Err(e) = sps_net::validation::verify_content_type(download_path, expected_ext) {
            tracing::error!("Content type verification failed: {}", e);
            // Attempt cleanup?
            let _ = fs::remove_dir_all(&actual_cask_room_version_path);
            return Err(e);
        }
    } else {
        tracing::debug!(
            "Skipping content type verification for {} (unknown/no expected extension: '{}')",
            download_path.display(),
            expected_ext
        );
    }
    match detected_extension.as_str() {
        "dmg" => {
            debug!(
                "Extracting DMG {} to stage {}...",
                download_path.display(),
                stage_path.display()
            );
            dmg::extract_dmg_to_stage(download_path, stage_path)?;
            debug!("Successfully extracted DMG to staging area.");
        }
        "zip" => {
            debug!(
                "Extracting ZIP {} to stage {}...",
                download_path.display(),
                stage_path.display()
            );
            extract::extract_archive(download_path, stage_path, 0, "zip")?;
            debug!("Successfully extracted ZIP to staging area.");
        }
        "gz" | "bz2" | "xz" | "tar" => {
            let archive_type_for_extraction = detected_extension.as_str();
            debug!(
                "Extracting TAR archive ({}) {} to stage {}...",
                archive_type_for_extraction,
                download_path.display(),
                stage_path.display()
            );
            extract::extract_archive(download_path, stage_path, 0, archive_type_for_extraction)?;
            debug!("Successfully extracted TAR archive to staging area.");
        }
        _ => {
            error!(
                "Unsupported container/installer type '{}' for staged installation derived from {}",
                detected_extension,
                download_path.display()
            );
            return Err(SpsError::Generic(format!(
                "Unsupported file type for staged installation: {detected_extension}"
            )));
        }
    }
    let mut all_installed_artifacts: Vec<InstalledArtifact> = Vec::new();
    let mut artifact_install_errors = Vec::new();
    if let Some(artifacts_def) = &cask.artifacts {
        debug!(
            "Processing {} declared artifacts from staging area...",
            artifacts_def.len()
        );
        for artifact_value in artifacts_def.iter() {
            if let Some(artifact_obj) = artifact_value.as_object() {
                if let Some((key, value)) = artifact_obj.iter().next() {
                    debug!("Processing artifact type: {}", key);
                    let result: Result<Vec<InstalledArtifact>> = match key.as_str() {
                        "app" => {
                            let mut app_artifacts = vec![];
                            if let Some(app_names) = value.as_array() {
                                for app_name_val in app_names {
                                    if let Some(app_name) = app_name_val.as_str() {
                                        let staged_app_path = stage_path.join(app_name);
                                        debug!(
                                            "Attempting to install app artifact: {}",
                                            staged_app_path.display()
                                        );
                                        match artifacts::app::install_app_from_staged(
                                            cask,
                                            &staged_app_path,
                                            &actual_cask_room_version_path,
                                            config,
                                            job_action, // Pass job_action for upgrade logic
                                        ) {
                                            Ok(mut artifacts) => {
                                                app_artifacts.append(&mut artifacts)
                                            }
                                            Err(e) => {
                                                return Err(e);
                                            }
                                        }
                                    } else {
                                        debug!(
                                            "Non-string value found in 'app' artifact array: {:?}",
                                            app_name_val
                                        );
                                    }
                                }
                            } else {
                                debug!("'app' artifact value is not an array: {:?}", value);
                            }
                            Ok(app_artifacts)
                        }
                        "pkg" => {
                            let mut installed_pkgs = vec![];
                            if let Some(pkg_names) = value.as_array() {
                                for pkg_val in pkg_names {
                                    if let Some(pkg_name) = pkg_val.as_str() {
                                        let staged_pkg_path = stage_path.join(pkg_name);
                                        debug!(
                                            "Attempting to install staged pkg artifact: {}",
                                            staged_pkg_path.display()
                                        );
                                        match artifacts::pkg::install_pkg_from_path(
                                            cask,
                                            &staged_pkg_path,
                                            &actual_cask_room_version_path, /* Pass actual
                                                                             * cask_room path */
                                            config,
                                        ) {
                                            Ok(mut artifacts) => {
                                                installed_pkgs.append(&mut artifacts)
                                            }
                                            Err(e) => {
                                                return Err(e);
                                            }
                                        }
                                    } else {
                                        debug!(
                                            "Non-string value found in 'pkg' artifact array: {:?}",
                                            pkg_val
                                        );
                                    }
                                }
                            } else {
                                debug!("'pkg' artifact value is not an array: {:?}", value);
                            }
                            Ok(installed_pkgs)
                        }
                        _ => {
                            debug!("Artifact type '{}' not supported yet — skipping.", key);
                            Ok(vec![])
                        }
                    };
                    match result {
                        Ok(installed) => {
                            if !installed.is_empty() {
                                debug!(
                                    "Successfully processed artifact '{}', added {} items.",
                                    key,
                                    installed.len()
                                );
                                all_installed_artifacts.extend(installed);
                            } else {
                                debug!(
                                    "Artifact handler for '{}' completed successfully but returned no artifacts.",
                                    key
                                );
                            }
                        }
                        Err(e) => {
                            error!("Error processing artifact '{}': {}", key, e);
                            artifact_install_errors.push(e);
                        }
                    }
                } else {
                    debug!("Empty artifact object found: {:?}", artifact_obj);
                }
            } else {
                debug!(
                    "Unexpected non-object artifact found in list: {:?}",
                    artifact_value
                );
            }
        }
    } else {
        error!(
            "Cask {} definition is missing the required 'artifacts' array. Cannot determine what to install.",
            cask.token
        );
        // Clean up the created actual_caskroom_version_path if no artifacts are defined
        let _ = fs::remove_dir_all(&actual_cask_room_version_path);
        return Err(SpsError::InstallError(format!(
            "Cask '{}' has no artifacts defined.",
            cask.token
        )));
    }
    if !artifact_install_errors.is_empty() {
        error!(
            "Encountered {} errors installing artifacts for cask '{}'. Installation incomplete.",
            artifact_install_errors.len(),
            cask.token
        );
        let _ = fs::remove_dir_all(&actual_cask_room_version_path); // Clean up actual cask_room on error
        return Err(artifact_install_errors.remove(0));
    }
    let actual_install_count = all_installed_artifacts
        .iter()
        .filter(|a| {
            !matches!(
                a,
                InstalledArtifact::PkgUtilReceipt { .. } | InstalledArtifact::Launchd { .. }
            )
        })
        .count();
    if actual_install_count == 0 {
        debug!(
            "No installable artifacts (like app, pkg, binary, etc.) were processed for cask '{}' from the staged content. Check cask definition.",
            cask.token
        );
        write_cask_manifest(
            cask,
            &actual_cask_room_version_path,
            all_installed_artifacts,
        )?;
    } else {
        debug!("Writing cask installation manifest");
        write_cask_manifest(
            cask,
            &actual_cask_room_version_path,
            all_installed_artifacts,
        )?;
    }
    debug!("Successfully installed cask: {}", cask.token);
    Ok(())
}

#[deprecated(note = "Use write_cask_manifest with detailed InstalledArtifact enum instead")]
pub fn write_receipt(
    cask: &Cask,
    cask_version_install_path: &Path,
    artifacts: Vec<String>,
) -> Result<()> {
    let receipt_path = cask_version_install_path.join("INSTALL_RECEIPT.json");
    debug!("Writing legacy cask receipt: {}", receipt_path.display());
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|e: SystemTimeError| SpsError::Generic(format!("System time error: {e}")))?
        .as_secs();
    let receipt_data = json!({
        "token": cask.token,
        "name": cask.name.as_ref().and_then(|n| n.first()).cloned(),
        "version": cask.version.as_ref().unwrap_or(&"latest".to_string()),
        "installed_at": timestamp,
        "artifacts_installed": artifacts
    });
    if let Some(parent) = receipt_path.parent() {
        fs::create_dir_all(parent).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
    }
    let mut file =
        fs::File::create(&receipt_path).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
    serde_json::to_writer_pretty(&mut file, &receipt_data)
        .map_err(|e| SpsError::Json(std::sync::Arc::new(e)))?;
    debug!(
        "Successfully wrote legacy receipt with {} artifact entries.",
        artifacts.len()
    );
    Ok(())
}

pub fn write_cask_manifest(
    cask: &Cask,
    cask_version_install_path: &Path,
    artifacts: Vec<InstalledArtifact>,
) -> Result<()> {
    let manifest_path = cask_version_install_path.join("CASK_INSTALL_MANIFEST.json");
    debug!("Writing cask manifest: {}", manifest_path.display());
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|e: SystemTimeError| SpsError::Generic(format!("System time error: {e}")))?
        .as_secs();

    // Determine primary app file name from artifacts
    let primary_app_file_name = artifacts.iter().find_map(|artifact| {
        if let InstalledArtifact::AppBundle { path } = artifact {
            path.file_name()
                .map(|name| name.to_string_lossy().to_string())
        } else {
            None
        }
    });

    // Always set is_installed=true when writing manifest (install or reinstall)
    // Try to determine cask_store_path from artifacts (AppBundle or CaskroomLink)
    let cask_store_path = artifacts.iter().find_map(|artifact| match artifact {
        InstalledArtifact::AppBundle { path } => Some(path.to_string_lossy().to_string()),
        InstalledArtifact::CaskroomLink { target_path, .. } => {
            Some(target_path.to_string_lossy().to_string())
        }
        _ => None,
    });

    let manifest_data = CaskInstallManifest {
        manifest_format_version: "1.0".to_string(),
        token: cask.token.clone(),
        version: cask.version.clone().unwrap_or_else(|| "latest".to_string()),
        installed_at: timestamp,
        artifacts,
        primary_app_file_name,
        is_installed: true,
        cask_store_path,
    };
    if let Some(parent) = manifest_path.parent() {
        fs::create_dir_all(parent).map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!("Failed create parent dir {}: {}", parent.display(), e),
            )))
        })?;
    }
    let file = fs::File::create(&manifest_path).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed create manifest {}: {}", manifest_path.display(), e),
        )))
    })?;
    let writer = std::io::BufWriter::new(file);
    serde_json::to_writer_pretty(writer, &manifest_data).map_err(|e| {
        error!(
            "Failed to serialize cask manifest JSON for {}: {}",
            cask.token, e
        );
        SpsError::Json(std::sync::Arc::new(e))
    })?;
    debug!(
        "Successfully wrote cask manifest with {} artifact entries.",
        manifest_data.artifacts.len()
    );
    Ok(())
}

/// Recursively cleans up empty parent directories in the private cask store.
/// Starts from the given path and walks up, removing empty directories until a non-empty or root is
/// found.
pub fn cleanup_empty_parent_dirs_in_private_store(start_path: &Path, stop_at: &Path) {
    let mut current = start_path.parent().unwrap_or(start_path).to_path_buf(); // Start from parent
    while current != *stop_at {
        if let Ok(read_dir) = fs::read_dir(&current) {
            if read_dir.count() == 0 {
                match fs::remove_dir(&current) {
                    Ok(_) => {
                        debug!("Removed empty directory: {}", current.display());
                    }
                    Err(e) => {
                        debug!("Failed to remove directory {}: {}", current.display(), e);
                        break;
                    }
                }
            } else {
                break;
            }
        } else {
            break;
        }
        if let Some(parent) = current.parent() {
            current = parent.to_path_buf();
        } else {
            break;
        }
    }
}

// ===== sps-core/src/build/cask/artifacts/zap.rs =====

use std::fs;
use std::path::{Path, PathBuf}; // Import Path
use std::process::{Command, Stdio};

use regex::Regex;
use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::InstalledArtifact;

/// Implements the `zap` stanza by performing deep-clean actions
/// such as trash, delete, rmdir, pkgutil forget, launchctl unload,
/// and arbitrary scripts, matching Homebrew's Cask behavior.
pub fn install_zap(cask: &Cask, config: &Config) -> Result<Vec<InstalledArtifact>> {
    let mut artifacts: Vec<InstalledArtifact> = Vec::new();
    let home = config.home_dir();

    if let Some(entries) = &cask.artifacts {
        for entry in entries {
            if let Some(obj) = entry.as_object() {
                if let Some(zaps) = obj.get("zap").and_then(|v| v.as_array()) {
                    for zap_map in zaps {
                        if let Some(zap_obj) = zap_map.as_object() {
                            for (key, val) in zap_obj {
                                match key.as_str() {
                                    "trash" => {
                                        if let Some(arr) = val.as_array() {
                                            for item in arr.iter().filter_map(|v| v.as_str()) {
                                                let target = expand_tilde(item, &home); // Pass &Path
                                                if !is_safe_path(&target, &home) {
                                                    debug!(
                                                        "Unsafe trash path {}, skipping",
                                                        target.display()
                                                    );
                                                    continue;
                                                }
                                                debug!("Trashing {}...", target.display());
                                                let _ = Command::new("trash")
                                                    .arg(&target)
                                                    .stdout(Stdio::null())
                                                    .stderr(Stdio::null())
                                                    .status();
                                            }
                                        }
                                    }
                                    "delete" => {
                                        if let Some(arr) = val.as_array() {
                                            for item in arr.iter().filter_map(|v| v.as_str()) {
                                                let target = expand_tilde(item, &home); // Pass &Path
                                                if !is_safe_path(&target, &home) {
                                                    debug!(
                                                        "Unsafe delete path {}, skipping",
                                                        target.display()
                                                    );
                                                    continue;
                                                }
                                                debug!("Deleting file {}...", target.display());
                                                if let Err(e) = fs::remove_file(&target) {
                                                    if e.kind() != std::io::ErrorKind::NotFound {
                                                        debug!(
                                                            "Failed to delete {}: {}",
                                                            target.display(),
                                                            e
                                                        );
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    "rmdir" => {
                                        if let Some(arr) = val.as_array() {
                                            for item in arr.iter().filter_map(|v| v.as_str()) {
                                                let target = expand_tilde(item, &home); // Pass &Path
                                                if !is_safe_path(&target, &home) {
                                                    debug!(
                                                        "Unsafe rmdir path {}, skipping",
                                                        target.display()
                                                    );
                                                    continue;
                                                }
                                                debug!(
                                                    "Removing directory {}...",
                                                    target.display()
                                                );
                                                if let Err(e) = fs::remove_dir_all(&target) {
                                                    if e.kind() != std::io::ErrorKind::NotFound {
                                                        debug!(
                                                            "Failed to rmdir {}: {}",
                                                            target.display(),
                                                            e
                                                        );
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    "pkgutil" => {
                                        if let Some(arr) = val.as_array() {
                                            for item in arr.iter().filter_map(|v| v.as_str()) {
                                                if !is_valid_pkgid(item) {
                                                    debug!(
                                                        "Invalid pkgutil id '{}', skipping",
                                                        item
                                                    );
                                                    continue;
                                                }
                                                debug!("Forgetting pkgutil receipt {}...", item);
                                                let _ = Command::new("pkgutil")
                                                    .arg("--forget")
                                                    .arg(item)
                                                    .stdout(Stdio::null())
                                                    .stderr(Stdio::null())
                                                    .status();
                                                artifacts.push(InstalledArtifact::PkgUtilReceipt {
                                                    id: item.to_string(),
                                                });
                                            }
                                        }
                                    }
                                    "launchctl" => {
                                        if let Some(arr) = val.as_array() {
                                            for label in arr.iter().filter_map(|v| v.as_str()) {
                                                if !is_valid_label(label) {
                                                    debug!(
                                                        "Invalid launchctl label '{}', skipping",
                                                        label
                                                    );
                                                    continue;
                                                }
                                                let plist = home // Use expanded home
                                                    .join("Library/LaunchAgents")
                                                    .join(format!("{label}.plist"));
                                                if !is_safe_path(&plist, &home) {
                                                    debug!(
                                                        "Unsafe plist path {} for label {}, skipping",
                                                        plist.display(),
                                                        label
                                                    );
                                                    continue;
                                                }
                                                debug!(
                                                    "Unloading launchctl {}...",
                                                    plist.display()
                                                );
                                                let _ = Command::new("launchctl")
                                                    .arg("unload")
                                                    .arg(&plist)
                                                    .stdout(Stdio::null())
                                                    .stderr(Stdio::null())
                                                    .status();
                                                artifacts.push(InstalledArtifact::Launchd {
                                                    label: label.to_string(),
                                                    path: Some(plist),
                                                });
                                            }
                                        }
                                    }
                                    "script" => {
                                        if let Some(cmd) = val.as_str() {
                                            if !is_valid_command(cmd) {
                                                debug!(
                                                    "Invalid zap script command '{}', skipping",
                                                    cmd
                                                );
                                                continue;
                                            }
                                            debug!("Running zap script: {}...", cmd);
                                            let _ = Command::new("sh")
                                                .arg("-c")
                                                .arg(cmd)
                                                .stdout(Stdio::null())
                                                .stderr(Stdio::null())
                                                .status();
                                        }
                                    }
                                    "signal" => {
                                        if let Some(arr) = val.as_array() {
                                            for cmd in arr.iter().filter_map(|v| v.as_str()) {
                                                if !is_valid_command(cmd) {
                                                    debug!(
                                                        "Invalid signal command '{}', skipping",
                                                        cmd
                                                    );
                                                    continue;
                                                }
                                                debug!("Running signal command: {}...", cmd);
                                                let _ = Command::new("sh")
                                                    .arg("-c")
                                                    .arg(cmd)
                                                    .stdout(Stdio::null())
                                                    .stderr(Stdio::null())
                                                    .status();
                                            }
                                        }
                                    }
                                    _ => debug!("Unsupported zap key '{}', skipping", key),
                                }
                            }
                        }
                    }
                    // Only process the first "zap" stanza found
                    break;
                }
            }
        }
    }

    Ok(artifacts)
}

// New helper functions to validate paths and strings.
fn is_safe_path(path: &Path, home: &Path) -> bool {
    if path
        .components()
        .any(|c| matches!(c, std::path::Component::ParentDir))
    {
        return false;
    }
    let path_str = path.to_string_lossy();
    if path.is_absolute()
        && (path_str.starts_with("/Applications") || path_str.starts_with("/Library"))
    {
        return true;
    }
    if path.starts_with(home) {
        return true;
    }
    if path_str.contains("Caskroom/Cellar") {
        return true;
    }
    false
}

fn is_valid_pkgid(pkgid: &str) -> bool {
    let re = Regex::new(r"^[a-zA-Z0-9.-]+$").unwrap();
    re.is_match(pkgid)
}

fn is_valid_label(label: &str) -> bool {
    let re = Regex::new(r"^[a-zA-Z0-9.-]+$").unwrap();
    re.is_match(label)
}

fn is_valid_command(cmd: &str) -> bool {
    let re = Regex::new(r"^[a-zA-Z0-9\s\-_./]+$").unwrap();
    re.is_match(cmd)
}

/// Expand a path that may start with '~' to the user's home directory
fn expand_tilde(path: &str, home: &Path) -> PathBuf {
    if let Some(stripped) = path.strip_prefix("~/") {
        home.join(stripped)
    } else {
        PathBuf::from(path)
    }
}

use std::fs;
use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::cask::Cask;
use tracing::{debug, error};

use crate::install::cask::InstalledArtifact; // Artifact type alias is just Value

/// Installs a PKG file and returns details of artifacts created/managed.
pub fn install_pkg_from_path(
    cask: &Cask,
    pkg_path: &Path,
    cask_version_install_path: &Path, // e.g., /opt/homebrew/Caskroom/foo/1.2.3
    _config: &Config,                 // Keep for potential future use
) -> Result<Vec<InstalledArtifact>> {
    // <-- Return type changed
    debug!("Installing pkg file: {}", pkg_path.display());

    if !pkg_path.exists() || !pkg_path.is_file() {
        return Err(SpsError::NotFound(format!(
            "Package file not found or is not a file: {}",
            pkg_path.display()
        )));
    }

    let pkg_name = pkg_path
        .file_name()
        .ok_or_else(|| SpsError::Generic(format!("Invalid pkg path: {}", pkg_path.display())))?;

    // --- Prepare list for artifacts ---
    let mut installed_artifacts: Vec<InstalledArtifact> = Vec::new();

    // --- Copy PKG to Caskroom for Reference ---
    let caskroom_pkg_path = cask_version_install_path.join(pkg_name);
    debug!(
        "Copying pkg to caskroom for reference: {}",
        caskroom_pkg_path.display()
    );
    if let Some(parent) = caskroom_pkg_path.parent() {
        fs::create_dir_all(parent).map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!("Failed create parent dir {}: {}", parent.display(), e),
            )))
        })?;
    }
    if let Err(e) = fs::copy(pkg_path, &caskroom_pkg_path) {
        error!(
            "Failed to copy PKG {} to {}: {}",
            pkg_path.display(),
            caskroom_pkg_path.display(),
            e
        );
        return Err(SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed copy PKG to caskroom: {e}"),
        ))));
    } else {
        // Record the reference copy artifact
        installed_artifacts.push(InstalledArtifact::CaskroomReference {
            path: caskroom_pkg_path.clone(),
        });
    }

    // --- Run Installer ---
    debug!("Running installer (this may require sudo)");
    debug!(
        "Executing: sudo installer -pkg {} -target /",
        pkg_path.display()
    );
    let output = Command::new("sudo")
        .arg("installer")
        .arg("-pkg")
        .arg(pkg_path)
        .arg("-target")
        .arg("/")
        .output()
        .map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!("Failed to execute sudo installer: {e}"),
            )))
        })?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        error!("sudo installer failed ({}): {}", output.status, stderr);
        // Don't clean up the reference copy here, let the main process handle directory removal on
        // failure
        return Err(SpsError::InstallError(format!(
            "Package installation failed for {}: {}",
            pkg_path.display(),
            stderr
        )));
    }
    debug!("Successfully ran installer command.");
    let stdout = String::from_utf8_lossy(&output.stdout);
    if !stdout.trim().is_empty() {
        debug!("Installer stdout:\n{}", stdout);
    }

    // --- Record PkgUtil Receipts (based on cask definition) ---
    if let Some(artifacts) = &cask.artifacts {
        // artifacts is Option<Vec<JsonValue>>
        for artifact_value in artifacts.iter() {
            if let Some(uninstall_array) =
                artifact_value.get("uninstall").and_then(|v| v.as_array())
            {
                for stanza_value in uninstall_array {
                    if let Some(stanza_obj) = stanza_value.as_object() {
                        if let Some(pkgutil_id) = stanza_obj.get("pkgutil").and_then(|v| v.as_str())
                        {
                            debug!("Found pkgutil ID to record: {}", pkgutil_id);
                            // Check for duplicates before adding
                            let new_artifact = InstalledArtifact::PkgUtilReceipt {
                                id: pkgutil_id.to_string(),
                            };
                            if !installed_artifacts.contains(&new_artifact) {
                                // Need PartialEq for InstalledArtifact
                                installed_artifacts.push(new_artifact);
                            }
                        }
                        // Consider other uninstall keys like launchctl, delete?
                    }
                }
            }
            // Optionally check "zap" stanzas too
        }
    }
    debug!("Successfully installed pkg: {}", pkg_path.display());
    Ok(installed_artifacts) // <-- Return collected artifacts
}

// sps-core/src/pipeline/worker.rs
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

use futures::executor::block_on;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::dependency::DependencyExt;
use sps_common::error::{Result as SpsResult, SpsError};
use sps_common::keg::KegRegistry;
use sps_common::model::formula::FormulaDependencies;
use sps_common::model::InstallTargetIdentifier;
use sps_common::pipeline::{JobAction, PipelineEvent, PipelinePackageType, WorkerJob};
use tokio::sync::broadcast;
use tracing::{debug, error, instrument, warn};

use crate::check::installed::{InstalledPackageInfo, PackageType as CorePackageType};
use crate::{build, install, uninstall, upgrade};

pub(super) fn execute_sync_job(
    worker_job: WorkerJob,
    config: &Config,
    cache: Arc<Cache>,
    event_tx: broadcast::Sender<PipelineEvent>,
) -> std::result::Result<(JobAction, PipelinePackageType), Box<(JobAction, SpsError)>> {
    let action = worker_job.request.action.clone();

    let result = do_execute_sync_steps(worker_job, config, cache, event_tx);

    result
        .map_err(|e| Box::new((action.clone(), e)))
        .map(|pkg_type| (action, pkg_type))
}

#[instrument(skip_all, fields(job_id = %worker_job.request.target_id, action = ?worker_job.request.action))]
fn do_execute_sync_steps(
    worker_job: WorkerJob,
    config: &Config,
    _cache: Arc<Cache>, // Marked as unused if cache is not directly used in this function body
    event_tx: broadcast::Sender<PipelineEvent>,
) -> SpsResult<PipelinePackageType> {
    let job_request = worker_job.request;
    let download_path = worker_job.download_path;
    let is_source_from_private_store = worker_job.is_source_from_private_store;

    let (core_pkg_type, pipeline_pkg_type) = match &job_request.target_definition {
        InstallTargetIdentifier::Formula(_) => {
            (CorePackageType::Formula, PipelinePackageType::Formula)
        }
        InstallTargetIdentifier::Cask(_) => (CorePackageType::Cask, PipelinePackageType::Cask),
    };

    // Check dependencies before proceeding with formula install/upgrade
    if let InstallTargetIdentifier::Formula(formula_arc) = &job_request.target_definition {
        if matches!(job_request.action, JobAction::Install)
            || matches!(job_request.action, JobAction::Upgrade { .. })
        {
            debug!(
                "[WORKER:{}] Pre-install check for dependencies. Formula: {}, Action: {:?}",
                job_request.target_id,
                formula_arc.name(),
                job_request.action
            );

            let keg_registry = KegRegistry::new(config.clone());
            match formula_arc.dependencies() {
                Ok(dependencies) => {
                    for dep in dependencies.runtime() {
                        debug!("[WORKER:{}] Checking runtime dependency: '{}'. Required by: '{}'. Configured cellar: {}", job_request.target_id, dep.name, formula_arc.name(), config.cellar_dir().display());

                        match keg_registry.get_installed_keg(&dep.name) {
                            Ok(Some(keg_info)) => {
                                debug!("[WORKER:{}] Dependency '{}' FOUND by KegRegistry. Path: {}, Version: {}", job_request.target_id, dep.name, keg_info.path.display(), keg_info.version_str);
                            }
                            Ok(None) => {
                                debug!("[WORKER:{}] Dependency '{}' was NOT FOUND by KegRegistry for formula '{}'. THIS IS THE ERROR POINT.", dep.name, job_request.target_id, formula_arc.name());
                                let error_msg = format!(
                                    "Runtime dependency '{}' for formula '{}' is not installed. Aborting operation for '{}'.",
                                    dep.name, formula_arc.name(), job_request.target_id
                                );
                                error!("{}", error_msg);
                                return Err(SpsError::DependencyError(error_msg));
                            }
                            Err(e) => {
                                debug!("[WORKER:{}] Error during KegRegistry check for dependency '{}': {}. Aborting for formula '{}'.", job_request.target_id, dep.name, e, job_request.target_id);
                                return Err(SpsError::Generic(format!(
                                    "Failed to check KegRegistry for {}: {}",
                                    dep.name, e
                                )));
                            }
                        }
                    }
                }
                Err(e) => {
                    let error_msg = format!(
                        "Could not retrieve dependency list for formula '{}': {}. Aborting operation.",
                        job_request.target_id, e
                    );
                    error!("{}", error_msg);
                    return Err(SpsError::DependencyError(error_msg));
                }
            }
            debug!(
                "[WORKER:{}] All required formula dependencies appear to be installed for '{}'.",
                job_request.target_id,
                formula_arc.name()
            );
        }
    }

    let mut formula_installed_path: Option<PathBuf> = None;

    match &job_request.action {
        JobAction::Upgrade {
            from_version,
            old_install_path,
        } => {
            debug!(
                "[{}] Upgrading from version {}",
                job_request.target_id, from_version
            );
            let old_info = InstalledPackageInfo {
                name: job_request.target_id.clone(),
                version: from_version.clone(),
                pkg_type: core_pkg_type.clone(),
                path: old_install_path.clone(),
            };

            match &job_request.target_definition {
                InstallTargetIdentifier::Formula(formula) => {
                    let http_client_for_bottle_upgrade = Arc::new(reqwest::Client::new());
                    let installed_path = if job_request.is_source_build {
                        let _ = event_tx.send(PipelineEvent::BuildStarted {
                            target_id: job_request.target_id.clone(),
                        });
                        let all_dep_paths = Vec::new(); // TODO: Populate this correctly if needed by upgrade_source_formula
                        block_on(upgrade::source::upgrade_source_formula(
                            formula,
                            &download_path,
                            &old_info,
                            config,
                            &all_dep_paths,
                        ))?
                    } else {
                        block_on(upgrade::bottle::upgrade_bottle_formula(
                            formula,
                            &download_path,
                            &old_info,
                            config,
                            http_client_for_bottle_upgrade,
                        ))?
                    };
                    formula_installed_path = Some(installed_path);
                }
                InstallTargetIdentifier::Cask(cask) => {
                    block_on(upgrade::cask::upgrade_cask_package(
                        cask,
                        &download_path,
                        &old_info,
                        config,
                    ))?;
                }
            }
        }
        JobAction::Install | JobAction::Reinstall { .. } => {
            if let JobAction::Reinstall {
                version: from_version,
                current_install_path: old_install_path,
            } = &job_request.action
            {
                debug!(
                    "[{}] Reinstall: Removing existing version {}...",
                    job_request.target_id, from_version
                );
                let _ = event_tx.send(PipelineEvent::UninstallStarted {
                    target_id: job_request.target_id.clone(),
                    version: from_version.clone(),
                });

                let old_info_for_reinstall = InstalledPackageInfo {
                    name: job_request.target_id.clone(),
                    version: from_version.clone(),
                    pkg_type: core_pkg_type.clone(),
                    path: old_install_path.clone(),
                };
                let uninstall_opts = uninstall::UninstallOptions { skip_zap: true };

                match core_pkg_type {
                    CorePackageType::Formula => uninstall::uninstall_formula_artifacts(
                        &old_info_for_reinstall,
                        config,
                        &uninstall_opts,
                    )?,
                    CorePackageType::Cask => {
                        uninstall::uninstall_cask_artifacts(&old_info_for_reinstall, config)?
                    }
                }
                debug!(
                    "[{}] Reinstall: Removed existing version {}.",
                    job_request.target_id, from_version
                );
                let _ = event_tx.send(PipelineEvent::UninstallFinished {
                    target_id: job_request.target_id.clone(),
                    version: from_version.clone(),
                });
            }

            let _ = event_tx.send(PipelineEvent::InstallStarted {
                target_id: job_request.target_id.clone(),
                pkg_type: pipeline_pkg_type,
            });

            match &job_request.target_definition {
                InstallTargetIdentifier::Formula(formula) => {
                    let install_dir_base =
                        (**formula).install_prefix(config.cellar_dir().as_path())?;
                    if let Some(parent_dir) = install_dir_base.parent() {
                        fs::create_dir_all(parent_dir).map_err(|e| SpsError::Io(Arc::new(e)))?;
                    }

                    if job_request.is_source_build {
                        debug!("[{}] Building from source...", job_request.target_id);
                        let _ = event_tx.send(PipelineEvent::BuildStarted {
                            target_id: job_request.target_id.clone(),
                        });
                        let build_dep_paths: Vec<PathBuf> = vec![]; // TODO: Populate this from ResolvedGraph

                        let build_future = build::compile::build_from_source(
                            &download_path,
                            formula,
                            config,
                            &build_dep_paths,
                        );
                        let installed_dir = block_on(build_future)?;
                        formula_installed_path = Some(installed_dir);
                    } else {
                        debug!("[{}] Installing bottle...", job_request.target_id);
                        let installed_dir =
                            install::bottle::exec::install_bottle(&download_path, formula, config)?;
                        formula_installed_path = Some(installed_dir);
                    }
                }
                InstallTargetIdentifier::Cask(cask) => {
                    if is_source_from_private_store {
                        debug!(
                            "[{}] Reinstalling cask from private store...",
                            job_request.target_id
                        );

                        if let Some(file_name) = download_path.file_name() {
                            let app_name = file_name.to_string_lossy().to_string();
                            let applications_app_path = config.applications_dir().join(&app_name);

                            if applications_app_path.exists()
                                || applications_app_path.symlink_metadata().is_ok()
                            {
                                debug!(
                                    "Removing existing app at {}",
                                    applications_app_path.display()
                                );
                                let _ = install::cask::helpers::remove_path_robustly(
                                    &applications_app_path,
                                    config,
                                    true,
                                );
                            }

                            debug!(
                                "Symlinking app from private store {} to {}",
                                download_path.display(),
                                applications_app_path.display()
                            );
                            if let Err(e) =
                                std::os::unix::fs::symlink(&download_path, &applications_app_path)
                            {
                                return Err(SpsError::InstallError(format!(
                                    "Failed to symlink app from private store to {}: {}",
                                    applications_app_path.display(),
                                    e
                                )));
                            }

                            let cask_version =
                                cask.version.clone().unwrap_or_else(|| "latest".to_string());
                            let cask_version_path =
                                config.cask_room_version_path(&cask.token, &cask_version);

                            if !cask_version_path.exists() {
                                fs::create_dir_all(&cask_version_path)?;
                            }

                            let caskroom_symlink_path = cask_version_path.join(&app_name);
                            if caskroom_symlink_path.exists()
                                || caskroom_symlink_path.symlink_metadata().is_ok()
                            {
                                let _ = fs::remove_file(&caskroom_symlink_path);
                            }

                            #[cfg(unix)]
                            {
                                if let Err(e) = std::os::unix::fs::symlink(
                                    &applications_app_path,
                                    &caskroom_symlink_path,
                                ) {
                                    warn!("Failed to create Caskroom symlink: {}", e);
                                }
                            }

                            let created_artifacts = vec![
                                sps_common::model::artifact::InstalledArtifact::AppBundle {
                                    path: applications_app_path.clone(),
                                },
                                sps_common::model::artifact::InstalledArtifact::CaskroomLink {
                                    link_path: caskroom_symlink_path.clone(),
                                    target_path: applications_app_path.clone(),
                                },
                            ];

                            debug!(
                                "[{}] Writing manifest for private store reinstall...",
                                job_request.target_id
                            );
                            if let Err(e) = install::cask::write_cask_manifest(
                                cask,
                                &cask_version_path,
                                created_artifacts,
                            ) {
                                error!(
                                    "[{}] Failed to write CASK_INSTALL_MANIFEST.json during private store reinstall: {}",
                                    job_request.target_id, e
                                );
                                return Err(SpsError::InstallError(format!(
                                    "Failed to write manifest during private store reinstall for {}: {}",
                                    job_request.target_id, e
                                )));
                            }
                        } else {
                            return Err(SpsError::InstallError(format!(
                                "Failed to get app name from private store path: {}",
                                download_path.display()
                            )));
                        }
                    } else {
                        debug!("[{}] Installing cask...", job_request.target_id);
                        install::cask::install_cask(
                            cask,
                            &download_path,
                            config,
                            &job_request.action,
                        )?;
                    }
                }
            }
        }
    };

    if let Some(ref installed_path) = formula_installed_path {
        debug!(
            "[{}] Formula operation resulted in keg path: {}",
            job_request.target_id,
            installed_path.display()
        );
    } else if core_pkg_type == CorePackageType::Cask {
        debug!("[{}] Cask operation completed.", job_request.target_id);
    }

    if let (InstallTargetIdentifier::Formula(formula), Some(keg_path_for_linking)) =
        (&job_request.target_definition, &formula_installed_path)
    {
        debug!(
            "[{}] Linking artifacts for formula {}...",
            job_request.target_id,
            (**formula).name()
        );
        let _ = event_tx.send(PipelineEvent::LinkStarted {
            target_id: job_request.target_id.clone(),
            pkg_type: pipeline_pkg_type,
        });
        install::bottle::link::link_formula_artifacts(formula, keg_path_for_linking, config)?;
        debug!(
            "[{}] Linking complete for formula {}.",
            job_request.target_id,
            (**formula).name()
        );
    }

    Ok(pipeline_pkg_type)
}

// ===== sps-core/src/build/formula/link.rs =====
use std::fs;
use std::io::Write;
use std::os::unix::fs as unix_fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use serde_json;
use sps_common::config::Config; // Import Config
use sps_common::error::{Result, SpsError};
use sps_common::model::formula::Formula;
use tracing::{debug, error};

const STANDARD_KEG_DIRS: [&str; 6] = ["bin", "lib", "share", "include", "etc", "Frameworks"];

/// Link all artifacts from a formula's installation directory.
// Added Config parameter
pub fn link_formula_artifacts(
    formula: &Formula,
    installed_keg_path: &Path,
    config: &Config, // Added config
) -> Result<()> {
    debug!(
        "Linking artifacts for {} from {}",
        formula.name(),
        installed_keg_path.display()
    );

    let formula_content_root = determine_content_root(installed_keg_path)?;
    let mut symlinks_created = Vec::<String>::new();

    // Use config methods for paths
    let opt_link_path = config.formula_opt_path(formula.name());
    let target_keg_dir = &formula_content_root;

    remove_existing_link_target(&opt_link_path)?;
    unix_fs::symlink(target_keg_dir, &opt_link_path).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed to create opt symlink for {}: {}", formula.name(), e),
        )))
    })?;
    symlinks_created.push(opt_link_path.to_string_lossy().to_string());
    debug!(
        "  Linked opt path: {} -> {}",
        opt_link_path.display(),
        target_keg_dir.display()
    );

    if let Some((base, _version)) = formula.name().split_once('@') {
        let alias_path = config.opt_dir().join(base); // Use config.opt_dir()
        if !alias_path.exists() {
            match unix_fs::symlink(target_keg_dir, &alias_path) {
                Ok(_) => {
                    debug!(
                        "  Added un‑versioned opt alias: {} -> {}",
                        alias_path.display(),
                        target_keg_dir.display()
                    );
                    symlinks_created.push(alias_path.to_string_lossy().to_string());
                }
                Err(e) => {
                    debug!(
                        "  Could not create opt alias {}: {}",
                        alias_path.display(),
                        e
                    );
                }
            }
        }
    }

    let standard_artifact_dirs = ["lib", "include", "share"];
    for dir_name in &standard_artifact_dirs {
        let source_subdir = formula_content_root.join(dir_name);
        // Use config.prefix() for target base
        let target_prefix_subdir = config.sps_root().join(dir_name);

        if source_subdir.is_dir() {
            fs::create_dir_all(&target_prefix_subdir)?;
            for entry in fs::read_dir(&source_subdir)? {
                let entry = entry?;
                let source_item_path = entry.path();
                let file_name = entry.file_name();
                if file_name.to_string_lossy().starts_with('.') {
                    continue;
                }

                let target_link = target_prefix_subdir.join(&file_name);
                remove_existing_link_target(&target_link)?;
                unix_fs::symlink(&source_item_path, &target_link).ok(); // ignore errors for individual links?
                symlinks_created.push(target_link.to_string_lossy().to_string());
                debug!(
                    "  Linked {} -> {}",
                    target_link.display(),
                    source_item_path.display()
                );
            }
        }
    }

    // Use config.bin_dir() for target bin
    let target_bin_dir = config.bin_dir();
    fs::create_dir_all(&target_bin_dir).ok();

    let source_bin_dir = formula_content_root.join("bin");
    if source_bin_dir.is_dir() {
        create_wrappers_in_dir(
            &source_bin_dir,
            &target_bin_dir,
            &formula_content_root,
            &mut symlinks_created,
        )?;
    }
    let source_libexec_dir = formula_content_root.join("libexec");
    if source_libexec_dir.is_dir() {
        create_wrappers_in_dir(
            &source_libexec_dir,
            &target_bin_dir,
            &formula_content_root,
            &mut symlinks_created,
        )?;
    }

    write_install_manifest(installed_keg_path, &symlinks_created)?;

    debug!(
        "Successfully completed linking artifacts for {}",
        formula.name()
    );
    Ok(())
}

// remove_existing_link_target, write_install_manifest remain mostly unchanged internally) ...
fn create_wrappers_in_dir(
    source_dir: &Path,
    target_bin_dir: &Path,
    formula_content_root: &Path,
    wrappers_created: &mut Vec<String>,
) -> Result<()> {
    debug!(
        "Scanning for executables in {} to create wrappers in {}",
        source_dir.display(),
        target_bin_dir.display()
    );
    match fs::read_dir(source_dir) {
        Ok(entries) => {
            for entry_result in entries {
                match entry_result {
                    Ok(entry) => {
                        let source_item_path = entry.path();
                        let file_name = entry.file_name();

                        if file_name.to_string_lossy().starts_with('.') {
                            continue;
                        }

                        if source_item_path.is_dir() {
                            create_wrappers_in_dir(
                                &source_item_path,
                                target_bin_dir,
                                formula_content_root,
                                wrappers_created,
                            )?;
                        } else if source_item_path.is_file() {
                            match is_executable(&source_item_path) {
                                Ok(true) => {
                                    let wrapper_path = target_bin_dir.join(&file_name);
                                    debug!("Found executable: {}", source_item_path.display());
                                    if remove_existing_link_target(&wrapper_path).is_ok() {
                                        debug!(
                                            "    Creating wrapper script: {} -> {}",
                                            wrapper_path.display(),
                                            source_item_path.display()
                                        );
                                        match create_wrapper_script(
                                            &source_item_path,
                                            &wrapper_path,
                                            formula_content_root,
                                        ) {
                                            Ok(_) => {
                                                debug!(
                                                    "  Created wrapper {} -> {}",
                                                    wrapper_path.display(),
                                                    source_item_path.display()
                                                );
                                                wrappers_created.push(
                                                    wrapper_path.to_string_lossy().to_string(),
                                                );
                                            }
                                            Err(e) => {
                                                error!(
                                                    "Failed to create wrapper script {} -> {}: {}",
                                                    wrapper_path.display(),
                                                    source_item_path.display(),
                                                    e
                                                );
                                            }
                                        }
                                    }
                                }
                                Ok(false) => { /* Not executable, ignore */ }
                                Err(e) => {
                                    debug!(
                                        "    Could not check executable status for {}: {}",
                                        source_item_path.display(),
                                        e
                                    );
                                }
                            }
                        }
                    }
                    Err(e) => {
                        debug!(
                            "  Failed to process directory entry in {}: {}",
                            source_dir.display(),
                            e
                        );
                    }
                }
            }
        }
        Err(e) => {
            debug!(
                "Failed to read source directory {}: {}",
                source_dir.display(),
                e
            );
        }
    }
    Ok(())
}
fn create_wrapper_script(
    target_executable: &Path,
    wrapper_path: &Path,
    formula_content_root: &Path,
) -> Result<()> {
    let libexec_path = formula_content_root.join("libexec");
    let perl_lib_path = libexec_path.join("lib").join("perl5");
    let python_lib_path = libexec_path.join("vendor"); // Assuming simple vendor dir

    let mut script_content = String::new();
    script_content.push_str("#!/bin/bash\n");
    script_content.push_str("# Wrapper script generated by sp\n");
    script_content.push_str("set -e\n\n");

    if perl_lib_path.exists() && perl_lib_path.is_dir() {
        script_content.push_str(&format!(
            "export PERL5LIB=\"{}:$PERL5LIB\"\n",
            perl_lib_path.display()
        ));
        debug!(
            "  (Wrapper will set PERL5LIB to {})",
            perl_lib_path.display()
        );
    }
    if python_lib_path.exists() && python_lib_path.is_dir() {
        script_content.push_str(&format!(
            "export PYTHONPATH=\"{}:$PYTHONPATH\"\n",
            python_lib_path.display()
        ));
        debug!(
            "  (Wrapper will set PYTHONPATH to {})",
            python_lib_path.display()
        );
    }

    script_content.push_str(&format!(
        "\nexec \"{}\" \"$@\"\n",
        target_executable.display()
    ));

    let mut file = fs::File::create(wrapper_path).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed create wrapper {}: {}", wrapper_path.display(), e),
        )))
    })?;
    file.write_all(script_content.as_bytes()).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed write wrapper {}: {}", wrapper_path.display(), e),
        )))
    })?;

    #[cfg(unix)]
    {
        let metadata = file.metadata()?;
        let mut permissions = metadata.permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(wrapper_path, permissions).map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!(
                    "Failed set wrapper executable {}: {}",
                    wrapper_path.display(),
                    e
                ),
            )))
        })?;
    }

    Ok(())
}

fn determine_content_root(installed_keg_path: &Path) -> Result<PathBuf> {
    let mut potential_subdirs = Vec::new();
    let mut top_level_files_found = false;
    if !installed_keg_path.is_dir() {
        error!(
            "Keg path {} does not exist or is not a directory!",
            installed_keg_path.display()
        );
        return Err(SpsError::NotFound(format!(
            "Keg path not found: {}",
            installed_keg_path.display()
        )));
    }
    match fs::read_dir(installed_keg_path) {
        Ok(entries) => {
            for entry_res in entries {
                if let Ok(entry) = entry_res {
                    let path = entry.path();
                    let file_name = entry.file_name();
                    // --- Use OsStr comparison ---
                    let file_name_osstr = file_name.as_os_str();
                    if file_name_osstr.to_string_lossy().starts_with('.')
                        || file_name_osstr == "INSTALL_MANIFEST.json"
                        || file_name_osstr == "INSTALL_RECEIPT.json"
                    {
                        continue;
                    }
                    if path.is_dir() {
                        // Store both path and name for check later
                        potential_subdirs.push((path, file_name.to_string_lossy().to_string()));
                    } else if path.is_file() {
                        top_level_files_found = true;
                        debug!(
                            "Found file '{}' at top level of keg {}, assuming no intermediate dir.",
                            file_name.to_string_lossy(), // Use lossy for display
                            installed_keg_path.display()
                        );
                        break; // Stop scanning if top-level files found
                    }
                } else {
                    debug!(
                        "Failed to read directory entry in {}: {}",
                        installed_keg_path.display(),
                        entry_res.err().unwrap() // Safe unwrap as we are in Err path
                    );
                }
            }
        }
        Err(e) => {
            debug!(
                "Could not read keg directory {} to check for intermediate dir: {}. Assuming keg path is content root.",
                installed_keg_path.display(),
                e
            );
            return Ok(installed_keg_path.to_path_buf());
        }
    }

    // --- MODIFIED LOGIC ---
    if potential_subdirs.len() == 1 && !top_level_files_found {
        // Get the single subdirectory path and name
        let (intermediate_dir_path, intermediate_dir_name) = potential_subdirs.remove(0); // Use remove

        // Check if the single directory name is one of the standard install dirs
        if STANDARD_KEG_DIRS.contains(&intermediate_dir_name.as_str()) {
            debug!(
                "Single directory found ('{}') is a standard directory. Using main keg directory {} as content root.",
                intermediate_dir_name,
                installed_keg_path.display()
            );
            Ok(installed_keg_path.to_path_buf()) // Use main keg path
        } else {
            // Single dir is NOT a standard name, assume it's an intermediate content root
            debug!(
                "Detected single non-standard intermediate content directory: {}",
                intermediate_dir_path.display()
            );
            Ok(intermediate_dir_path) // Use the intermediate dir
        }
    // --- END MODIFIED LOGIC ---
    } else {
        // Handle multiple subdirs or top-level files found case (no change needed here)
        if potential_subdirs.len() > 1 {
            debug!(
                "Multiple potential content directories found under keg {}. Using main keg directory as content root.",
                installed_keg_path.display()
            );
        } else if top_level_files_found {
            debug!(
                "Top-level files found in keg {}. Using main keg directory as content root.",
                installed_keg_path.display()
            );
        } else if potential_subdirs.is_empty() {
            // Changed from else if to else
            debug!(
                "No subdirectories or files found (excluding ignored ones) in keg {}. Using main keg directory as content root.",
                installed_keg_path.display()
            );
        }
        Ok(installed_keg_path.to_path_buf()) // Use main keg path in these cases too
    }
}

fn remove_existing_link_target(path: &Path) -> Result<()> {
    match path.symlink_metadata() {
        Ok(metadata) => {
            debug!(
                "    Removing existing item at link target: {}",
                path.display()
            );
            let is_dir = metadata.file_type().is_dir();
            let is_symlink = metadata.file_type().is_symlink();
            let is_real_dir = is_dir && !is_symlink;
            let remove_result = if is_real_dir {
                fs::remove_dir_all(path)
            } else {
                fs::remove_file(path)
            };
            if let Err(e) = remove_result {
                debug!(
                    "    Failed to remove existing item at link target {}: {}",
                    path.display(),
                    e
                );
                return Err(SpsError::Io(std::sync::Arc::new(e)));
            }
            Ok(())
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(e) => {
            debug!(
                "    Failed to get metadata for existing item {}: {}",
                path.display(),
                e
            );
            Err(SpsError::Io(std::sync::Arc::new(e)))
        }
    }
}

fn write_install_manifest(installed_keg_path: &Path, symlinks_created: &[String]) -> Result<()> {
    let manifest_path = installed_keg_path.join("INSTALL_MANIFEST.json");
    debug!("Writing install manifest to: {}", manifest_path.display());
    match serde_json::to_string_pretty(&symlinks_created) {
        Ok(manifest_json) => match fs::write(&manifest_path, manifest_json) {
            Ok(_) => {
                debug!(
                    "Wrote install manifest with {} links: {}",
                    symlinks_created.len(),
                    manifest_path.display()
                );
            }
            Err(e) => {
                error!(
                    "Failed to write install manifest {}: {}",
                    manifest_path.display(),
                    e
                );
                return Err(SpsError::Io(std::sync::Arc::new(e)));
            }
        },
        Err(e) => {
            error!("Failed to serialize install manifest data: {}", e);
            return Err(SpsError::Json(std::sync::Arc::new(e)));
        }
    }
    Ok(())
}

pub fn unlink_formula_artifacts(
    formula_name: &str,
    version_str_full: &str, // e.g., "1.2.3_1"
    config: &Config,
) -> Result<()> {
    debug!(
        "Unlinking artifacts for {} version {}",
        formula_name, version_str_full
    );
    // Use config method to get expected keg path based on name and version string
    let expected_keg_path = config.formula_keg_path(formula_name, version_str_full);
    let manifest_path = expected_keg_path.join("INSTALL_MANIFEST.json"); // Manifest *inside* the keg

    if manifest_path.is_file() {
        debug!("Reading install manifest: {}", manifest_path.display());
        match fs::read_to_string(&manifest_path) {
            Ok(manifest_str) => {
                match serde_json::from_str::<Vec<String>>(&manifest_str) {
                    Ok(links_to_remove) => {
                        let mut unlinked_count = 0;
                        let mut removal_errors = 0;
                        if links_to_remove.is_empty() {
                            debug!(
                                "Install manifest {} is empty. Cannot perform manifest-based unlink.",
                                manifest_path.display()
                            );
                        } else {
                            // Use Config to get base paths for checking ownership/safety
                            let opt_base = config.opt_dir();
                            let bin_base = config.bin_dir();
                            let lib_base = config.sps_root().join("lib");
                            let include_base = config.sps_root().join("include");
                            let share_base = config.sps_root().join("share");
                            // Add etc, sbin etc. if needed

                            for link_str in links_to_remove {
                                let link_path = PathBuf::from(link_str);
                                // Check if it's under a managed directory (safety check)
                                if link_path.starts_with(&opt_base)
                                    || link_path.starts_with(&bin_base)
                                    || link_path.starts_with(&lib_base)
                                    || link_path.starts_with(&include_base)
                                    || link_path.starts_with(&share_base)
                                {
                                    match remove_existing_link_target(&link_path) {
                                        // Use helper
                                        Ok(_) => {
                                            debug!("Removed link/wrapper: {}", link_path.display());
                                            unlinked_count += 1;
                                        }
                                        Err(e) => {
                                            // Log error but continue trying to remove others
                                            debug!(
                                                "Failed to remove link/wrapper {}: {}",
                                                link_path.display(),
                                                e
                                            );
                                            removal_errors += 1;
                                        }
                                    }
                                } else {
                                    // This indicates a potentially corrupted manifest or a link
                                    // outside expected areas
                                    error!(
                                        "Manifest contains unexpected link path, skipping removal: {}",
                                        link_path.display()
                                    );
                                    removal_errors += 1; // Count as an error/problem
                                }
                            }
                        }
                        debug!(
                            "Attempted to unlink {} artifacts based on manifest.",
                            unlinked_count
                        );
                        if removal_errors > 0 {
                            error!(
                                "Encountered {} errors while removing links listed in manifest.",
                                removal_errors
                            );
                            // Decide if this should be a hard error - perhaps not if keg is being
                            // removed anyway? For now, just log
                            // warnings.
                        }
                        Ok(()) // Return Ok even if some links failed, keg removal will happen next
                    }
                    Err(e) => {
                        error!(
                            "Failed to parse formula install manifest {}: {}. Proceeding without detailed unlink.",
                            manifest_path.display(),
                            e
                        );
                        // Don't error out, allow keg removal to proceed.
                        Ok(())
                    }
                }
            }
            Err(e) => {
                error!(
                    "Failed to read formula install manifest {}: {}. Proceeding without detailed unlink.",
                    manifest_path.display(),
                    e
                );
                // Don't error out, allow keg removal to proceed.
                Ok(())
            }
        }
    } else {
        debug!(
            "Warning: No install manifest found at {}. Cannot perform detailed unlink.",
            manifest_path.display()
        );
        // Don't error out, allow keg removal to proceed.
        Ok(())
    }
}

fn is_executable(path: &Path) -> Result<bool> {
    if !path.try_exists().unwrap_or(false) || !path.is_file() {
        return Ok(false);
    }
    if cfg!(unix) {
        use std::os::unix::fs::PermissionsExt;
        match fs::metadata(path) {
            Ok(metadata) => Ok(metadata.permissions().mode() & 0o111 != 0),
            Err(e) => Err(SpsError::Io(std::sync::Arc::new(e))),
        }
    } else {
        Ok(true)
    }
}

use std::collections::{HashMap, HashSet};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::os::unix::fs::{symlink, PermissionsExt};
use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio};
use std::sync::Arc;

use reqwest::Client;
use semver;
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::formula::{BottleFileSpec, Formula, FormulaDependencies};
use sps_net::http::ProgressCallback;
use sps_net::oci;
use sps_net::validation::verify_checksum;
use tempfile::NamedTempFile;
use tracing::{debug, error, warn};
use walkdir::WalkDir;

use super::macho;
use crate::install::bottle::get_current_platform;
use crate::install::extract::extract_archive;

pub async fn download_bottle(
    formula: &Formula,
    config: &Config,
    client: &Client,
) -> Result<PathBuf> {
    download_bottle_with_progress(formula, config, client, None).await
}

pub async fn download_bottle_with_progress(
    formula: &Formula,
    config: &Config,
    client: &Client,
    progress_callback: Option<ProgressCallback>,
) -> Result<PathBuf> {
    download_bottle_with_progress_and_cache_info(formula, config, client, progress_callback)
        .await
        .map(|(path, _)| path)
}

pub async fn download_bottle_with_progress_and_cache_info(
    formula: &Formula,
    config: &Config,
    client: &Client,
    progress_callback: Option<ProgressCallback>,
) -> Result<(PathBuf, bool)> {
    debug!("Attempting to download bottle for {}", formula.name);
    let (platform_tag, bottle_file_spec) = get_bottle_for_platform(formula)?;
    debug!(
        "Selected bottle spec for platform '{}': URL={}, SHA256={}",
        platform_tag, bottle_file_spec.url, bottle_file_spec.sha256
    );
    if bottle_file_spec.url.is_empty() {
        return Err(SpsError::DownloadError(
            formula.name.clone(),
            "".to_string(),
            "Bottle spec has an empty URL.".to_string(),
        ));
    }
    let standard_version_str = formula.version_str_full();
    let filename = format!(
        "{}-{}.{}.bottle.tar.gz",
        formula.name, standard_version_str, platform_tag
    );
    let cache_dir = config.cache_dir().join("bottles");
    fs::create_dir_all(&cache_dir).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
    let bottle_cache_path = cache_dir.join(&filename);
    if bottle_cache_path.is_file() {
        debug!("Bottle found in cache: {}", bottle_cache_path.display());
        if !bottle_file_spec.sha256.is_empty() {
            match verify_checksum(&bottle_cache_path, &bottle_file_spec.sha256) {
                Ok(_) => {
                    debug!("Using valid cached bottle: {}", bottle_cache_path.display());
                    return Ok((bottle_cache_path, true));
                }
                Err(e) => {
                    debug!(
                        "Cached bottle checksum mismatch ({}): {}. Redownloading.",
                        bottle_cache_path.display(),
                        e
                    );
                    let _ = fs::remove_file(&bottle_cache_path);
                }
            }
        } else {
            warn!(
                "Using cached bottle without checksum verification (checksum not specified): {}",
                bottle_cache_path.display()
            );
            return Ok((bottle_cache_path, true));
        }
    } else {
        debug!("Bottle not found in cache.");
    }
    let bottle_url_str = &bottle_file_spec.url;
    let registry_domain = config
        .artifact_domain
        .as_deref()
        .unwrap_or(oci::DEFAULT_GHCR_DOMAIN);
    let is_oci_blob_url = (bottle_url_str.contains("://ghcr.io/")
        || bottle_url_str.contains(registry_domain))
        && bottle_url_str.contains("/blobs/sha256:");
    debug!(
        "Checking URL type: '{}'. Is OCI Blob URL? {}",
        bottle_url_str, is_oci_blob_url
    );
    if is_oci_blob_url {
        let expected_digest = bottle_url_str.split("/blobs/sha256:").nth(1).unwrap_or("");
        if expected_digest.is_empty() {
            warn!(
                "Could not extract expected SHA256 digest from OCI URL: {}",
                bottle_url_str
            );
        }
        debug!(
            "Detected OCI blob URL, initiating direct blob download: {} (Digest: {})",
            bottle_url_str, expected_digest
        );
        match oci::download_oci_blob_with_progress(
            bottle_url_str,
            &bottle_cache_path,
            config,
            client,
            expected_digest,
            progress_callback.clone(),
        )
        .await
        {
            Ok(_) => {
                debug!(
                    "Successfully downloaded OCI blob to {}",
                    bottle_cache_path.display()
                );
            }
            Err(e) => {
                error!("Failed to download OCI blob from {}: {}", bottle_url_str, e);
                let _ = fs::remove_file(&bottle_cache_path);
                return Err(SpsError::DownloadError(
                    formula.name.clone(),
                    bottle_url_str.to_string(),
                    format!("Failed to download OCI blob: {e}"),
                ));
            }
        }
    } else {
        debug!(
            "Detected standard HTTPS URL, using direct download for: {}",
            bottle_url_str
        );
        match sps_net::http::fetch_formula_source_or_bottle_with_progress(
            formula.name(),
            bottle_url_str,
            &bottle_file_spec.sha256,
            &[],
            config,
            progress_callback,
        )
        .await
        {
            Ok(downloaded_path) => {
                if downloaded_path != bottle_cache_path {
                    debug!(
                        "fetch_formula_source_or_bottle returned path {}. Expected: {}. Assuming correct.",
                        downloaded_path.display(),
                        bottle_cache_path.display()
                    );
                    if !bottle_cache_path.exists() {
                        error!(
                            "Downloaded path {} exists, but expected final cache path {} does not!",
                            downloaded_path.display(),
                            bottle_cache_path.display()
                        );
                        return Err(SpsError::Generic(format!(
                            "Download path mismatch and final file missing: {}",
                            bottle_cache_path.display()
                        )));
                    }
                }
                debug!(
                    "Successfully downloaded directly to {}",
                    bottle_cache_path.display()
                );
            }
            Err(e) => {
                error!("Failed to download directly from {}: {}", bottle_url_str, e);
                return Err(e);
            }
        }
    }
    if !bottle_cache_path.exists() {
        error!(
            "Bottle download process completed, but the final file {} does not exist.",
            bottle_cache_path.display()
        );
        return Err(SpsError::Generic(format!(
            "Bottle file missing after download attempt: {}",
            bottle_cache_path.display()
        )));
    }
    debug!(
        "Bottle download successful: {}",
        bottle_cache_path.display()
    );
    Ok((bottle_cache_path, false))
}

pub fn get_bottle_for_platform(formula: &Formula) -> Result<(String, &BottleFileSpec)> {
    let stable_spec = formula.bottle.stable.as_ref().ok_or_else(|| {
        SpsError::Generic(format!(
            "Formula '{}' has no stable bottle specification.",
            formula.name
        ))
    })?;
    if stable_spec.files.is_empty() {
        return Err(SpsError::Generic(format!(
            "Formula '{}' has no bottle files listed in stable spec.",
            formula.name
        )));
    }
    let current_platform = get_current_platform();
    if current_platform == "unknown" || current_platform.contains("unknown") {
        debug!(
            "Could not reliably determine current platform ('{}'). Bottle selection might be incorrect.",
            current_platform
        );
    }
    debug!(
        "Determining bottle for current platform: {}",
        current_platform
    );
    debug!(
        "Available bottle platforms in formula spec: {:?}",
        stable_spec.files.keys().cloned().collect::<Vec<_>>()
    );
    if let Some(spec) = stable_spec.files.get(&current_platform) {
        debug!(
            "Found exact bottle match for platform: {}",
            current_platform
        );
        return Ok((current_platform.clone(), spec));
    }
    debug!("No exact match found for {}", current_platform);
    const ARM_MACOS_VERSIONS: &[&str] = &["sequoia", "sonoma", "ventura", "monterey", "big_sur"];
    const INTEL_MACOS_VERSIONS: &[&str] = &[
        "sequoia", "sonoma", "ventura", "monterey", "big_sur", "catalina", "mojave",
    ];
    if cfg!(target_os = "macos") {
        if let Some(current_os_name) = current_platform
            .strip_prefix("arm64_")
            .or(Some(current_platform.as_str()))
        {
            let version_list = if current_platform.starts_with("arm64_") {
                ARM_MACOS_VERSIONS
            } else {
                INTEL_MACOS_VERSIONS
            };
            if let Some(current_os_index) = version_list.iter().position(|&v| v == current_os_name)
            {
                for target_os_name in version_list.iter().skip(current_os_index) {
                    let target_tag = if current_platform.starts_with("arm64_") {
                        format!("arm64_{target_os_name}")
                    } else {
                        target_os_name.to_string()
                    };
                    if let Some(spec) = stable_spec.files.get(&target_tag) {
                        debug!(
                            "No bottle found for exact platform '{}'. Using compatible older bottle '{}'.",
                            current_platform, target_tag
                        );
                        return Ok((target_tag, spec));
                    }
                }
                debug!(
                    "Checked compatible older macOS versions ({:?}), no suitable bottle found.",
                    &version_list[current_os_index..]
                );
            } else {
                debug!(
                    "Current OS '{}' not found in known macOS version list.",
                    current_os_name
                );
            }
        } else {
            debug!(
                "Could not extract OS name from platform tag '{}'",
                current_platform
            );
        }
    }
    if current_platform.starts_with("arm64_") {
        if let Some(spec) = stable_spec.files.get("arm64_big_sur") {
            debug!(
                "No specific OS bottle found for {}. Falling back to 'arm64_big_sur' bottle.",
                current_platform
            );
            return Ok(("arm64_big_sur".to_string(), spec));
        }
        debug!("No 'arm64_big_sur' fallback bottle tag found.");
    } else if cfg!(target_os = "macos") {
        if let Some(spec) = stable_spec.files.get("big_sur") {
            debug!(
                "No specific OS bottle found for {}. Falling back to 'big_sur' bottle.",
                current_platform
            );
            return Ok(("big_sur".to_string(), spec));
        }
        debug!("No 'big_sur' fallback bottle tag found.");
    }
    if let Some(spec) = stable_spec.files.get("all") {
        debug!(
            "No platform-specific or OS-specific bottle found for {}. Using 'all' platform bottle.",
            current_platform
        );
        return Ok(("all".to_string(), spec));
    }
    debug!("No 'all' platform bottle found.");
    Err(SpsError::DownloadError(
        formula.name.clone(),
        "".to_string(),
        format!(
            "No compatible bottle found for platform '{}'. Available: {:?}",
            current_platform,
            stable_spec.files.keys().collect::<Vec<_>>()
        ),
    ))
}

pub fn install_bottle(bottle_path: &Path, formula: &Formula, config: &Config) -> Result<PathBuf> {
    let install_dir = formula.install_prefix(config.cellar_dir().as_path())?;
    if install_dir.exists() {
        debug!(
            "Removing existing keg directory before installing: {}",
            install_dir.display()
        );
        fs::remove_dir_all(&install_dir).map_err(|e| {
            SpsError::InstallError(format!(
                "Failed to remove existing keg {}: {}",
                install_dir.display(),
                e
            ))
        })?;
    }
    if let Some(parent_dir) = install_dir.parent() {
        fs::create_dir_all(parent_dir).map_err(|e| {
            SpsError::Io(std::sync::Arc::new(std::io::Error::new(
                e.kind(),
                format!(
                    "Failed to create parent dir {}: {}",
                    parent_dir.display(),
                    e
                ),
            )))
        })?;
    } else {
        return Err(SpsError::InstallError(format!(
            "Could not determine parent directory for install path: {}",
            install_dir.display()
        )));
    }
    fs::create_dir_all(&install_dir).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed to create keg dir {}: {}", install_dir.display(), e),
        )))
    })?;
    let strip_components = 2;
    debug!(
        "Extracting bottle archive {} to {} with strip_components={}",
        bottle_path.display(),
        install_dir.display(),
        strip_components
    );
    extract_archive(bottle_path, &install_dir, strip_components, "gz")?;
    debug!(
        "Ensuring write permissions for extracted files in {}",
        install_dir.display()
    );
    ensure_write_permissions(&install_dir)?;
    debug!("Performing bottle relocation in {}", install_dir.display());
    perform_bottle_relocation(formula, &install_dir, config)?;
    ensure_llvm_symlinks(&install_dir, formula, config)?;
    crate::install::bottle::write_receipt(formula, &install_dir, "bottle")?;
    debug!(
        "Bottle installation complete for {} at {}",
        formula.name(),
        install_dir.display()
    );
    Ok(install_dir)
}

fn ensure_write_permissions(path: &Path) -> Result<()> {
    if !path.exists() {
        debug!(
            "Path {} does not exist, cannot ensure write permissions.",
            path.display()
        );
        return Ok(());
    }
    for entry_result in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
        let entry_path = entry_result.path();
        if entry_path == path && entry_result.depth() == 0 {
            continue;
        }
        match fs::metadata(entry_path) {
            Ok(metadata) => {
                let mut perms = metadata.permissions();
                let _is_readonly = perms.readonly();
                #[cfg(unix)]
                {
                    let current_mode = perms.mode();
                    let new_mode = current_mode | 0o200;
                    if new_mode != current_mode {
                        perms.set_mode(new_mode);
                        let _ = fs::set_permissions(entry_path, perms);
                    }
                }
                #[cfg(not(unix))]
                {
                    if _is_readonly {
                        perms.set_readonly(false);
                        let _ = fs::set_permissions(entry_path, perms);
                    }
                }
            }
            Err(_e) => {}
        }
    }
    Ok(())
}

fn perform_bottle_relocation(formula: &Formula, install_dir: &Path, config: &Config) -> Result<()> {
    let mut repl: HashMap<String, String> = HashMap::new();
    repl.insert(
        "@@HOMEBREW_CELLAR@@".into(),
        config.cellar_dir().to_string_lossy().into(),
    );
    repl.insert(
        "@@HOMEBREW_PREFIX@@".into(),
        config.sps_root().to_string_lossy().into(),
    );
    let _prefix_path_str = config.sps_root().to_string_lossy();
    let library_path_str = config
        .sps_root()
        .join("Library")
        .to_string_lossy()
        .to_string(); // Assuming Library is under sps_root for this placeholder
                      // HOMEBREW_REPOSITORY usually points to the Homebrew/brew git repo, not relevant for sps in
                      // this context. If needed for a specific formula, it should point to
                      // /opt/sps or similar.
    repl.insert(
        "@@HOMEBREW_REPOSITORY@@".into(),
        config.sps_root().to_string_lossy().into(),
    );
    repl.insert("@@HOMEBREW_LIBRARY@@".into(), library_path_str.to_string());

    let formula_opt_path = config.formula_opt_path(formula.name());
    let formula_opt_str = formula_opt_path.to_string_lossy();
    let install_dir_str = install_dir.to_string_lossy();
    if formula_opt_str != install_dir_str {
        repl.insert(formula_opt_str.to_string(), install_dir_str.to_string());
        debug!(
            "Adding self-opt relocation: {} -> {}",
            formula_opt_str, install_dir_str
        );
    }

    if formula.name().starts_with("python@") {
        let version_full = formula.version_str_full();
        let mut parts = version_full.split('.');
        if let (Some(major), Some(minor)) = (parts.next(), parts.next()) {
            let framework_version = format!("{major}.{minor}");
            let framework_dir = install_dir
                .join("Frameworks")
                .join("Python.framework")
                .join("Versions")
                .join(&framework_version);
            let python_lib = framework_dir.join("Python");
            let python_bin = framework_dir
                .join("bin")
                .join(format!("python{major}.{minor}"));

            let absolute_python_lib_path_obj = install_dir
                .join("Frameworks")
                .join("Python.framework")
                .join("Versions")
                .join(&framework_version)
                .join("Python");
            let new_id_abs = absolute_python_lib_path_obj.to_str().ok_or_else(|| {
                SpsError::InstallError(format!(
                    "Failed to convert absolute Python library path to string: {}",
                    absolute_python_lib_path_obj.display()
                ))
            })?;

            debug!(
                "Setting absolute ID for {}: {}",
                python_lib.display(),
                new_id_abs
            );
            let status_id = StdCommand::new("install_name_tool")
                .args(["-id", new_id_abs, python_lib.to_str().unwrap()])
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .status()
                .map_err(|e| SpsError::Io(Arc::new(e)))?;
            if !status_id.success() {
                error!("install_name_tool -id failed for {}", python_lib.display());
                return Err(SpsError::InstallError(format!(
                    "Failed to set absolute id on Python dynamic library: {}",
                    python_lib.display()
                )));
            }

            debug!("Skipping -add_rpath as absolute paths are used for Python linkage.");

            let old_load_placeholder = format!(
                "@@HOMEBREW_CELLAR@@/{}/{}/Frameworks/Python.framework/Versions/{}/Python",
                formula.name(),
                version_full,
                framework_version
            );
            let old_load_resource_placeholder = format!(
                "@@HOMEBREW_CELLAR@@/{}/{}/Frameworks/Python.framework/Versions/{}/Resources/Python.app/Contents/MacOS/Python",
                formula.name(),
                version_full,
                framework_version
            );
            let install_dir_str_ref = install_dir.to_string_lossy();
            let abs_old_load = format!(
                "{install_dir_str_ref}/Frameworks/Python.framework/Versions/{framework_version}/Python"
            );
            let abs_old_load_resource = format!(
                "{install_dir_str_ref}/Frameworks/Python.framework/Versions/{framework_version}/Resources/Python.app/Contents/MacOS/Python"
            );

            let run_change = |old: &str, new: &str, target: &Path| -> Result<()> {
                if !target.exists() {
                    debug!(
                        "Target {} does not exist, skipping install_name_tool -change.",
                        target.display()
                    );
                    return Ok(());
                }
                debug!(
                    "Running install_name_tool -change {} {} {}",
                    old,
                    new,
                    target.display()
                );
                let output = StdCommand::new("install_name_tool")
                    .args(["-change", old, new, target.to_str().unwrap()])
                    .output()
                    .map_err(|e| SpsError::Io(Arc::new(e)))?;

                if !output.status.success() {
                    let stderr = String::from_utf8_lossy(&output.stderr);
                    if !stderr.contains("file not found")
                        && !stderr.contains("no LC_LOAD_DYLIB command specifying file")
                        && !stderr.contains("is not a Mach-O file")
                        && !stderr.contains("object file format invalid")
                        && !stderr.trim().is_empty()
                    {
                        error!(
                            "install_name_tool -change failed unexpectedly for target {}: {}",
                            target.display(),
                            stderr.trim()
                        );
                    } else {
                        debug!(
                            "install_name_tool -change: old path '{}' likely not found or target '{}' not relevant (stderr: {}).",
                            old,
                            target.display(),
                            stderr.trim()
                        );
                    }
                }
                Ok(())
            };

            debug!("Patching main executable: {}", python_bin.display());
            run_change(&old_load_placeholder, new_id_abs, &python_bin)?;
            run_change(&old_load_resource_placeholder, new_id_abs, &python_bin)?;
            run_change(&abs_old_load, new_id_abs, &python_bin)?;
            run_change(&abs_old_load_resource, new_id_abs, &python_bin)?;

            let python_app = framework_dir
                .join("Resources")
                .join("Python.app")
                .join("Contents")
                .join("MacOS")
                .join("Python");

            if python_app.exists() {
                debug!(
                    "Explicitly patching Python.app executable: {}",
                    python_app.display()
                );
                run_change(&old_load_placeholder, new_id_abs, &python_app)?;
                run_change(&abs_old_load, new_id_abs, &python_app)?;
                run_change(&old_load_resource_placeholder, new_id_abs, &python_app)?;
                run_change(&abs_old_load_resource, new_id_abs, &python_app)?;
            } else {
                warn!(
                    "Python.app executable not found at {}, skipping explicit patch.",
                    python_app.display()
                );
            }

            codesign_path(&python_lib)?;
            codesign_path(&python_bin)?;
            if python_app.exists() {
                codesign_path(&python_app)?;
            } else {
                debug!(
                    "Python.app binary not found at {}, skipping codesign.",
                    python_app.display()
                );
            }
        }
    }

    if let Some(perl_path) = find_brewed_perl(config.sps_root()).or_else(|| {
        if cfg!(target_os = "macos") {
            Some(PathBuf::from("/usr/bin/perl"))
        } else {
            None
        }
    }) {
        repl.insert(
            "@@HOMEBREW_PERL@@".into(),
            perl_path.to_string_lossy().into(),
        );
    }

    match formula.dependencies() {
        Ok(deps) => {
            if let Some(openjdk) = deps
                .iter()
                .find(|d| d.name.starts_with("openjdk"))
                .map(|d| d.name.clone())
            {
                let openjdk_opt = config.formula_opt_path(&openjdk);
                repl.insert(
                    "@@HOMEBREW_JAVA@@".into(),
                    openjdk_opt
                        .join("libexec/openjdk.jdk/Contents/Home")
                        .to_string_lossy()
                        .into(),
                );
            }
        }
        Err(e) => {
            warn!(
                "Could not check formula dependencies during relocation for {}: {}",
                formula.name(),
                e
            );
        }
    }

    repl.insert("HOMEBREW_RELOCATE_RPATHS".into(), "1".into());

    let opt_placeholder = format!(
        "@@HOMEBREW_OPT_{}@@",
        formula.name().to_uppercase().replace(['-', '+', '.'], "_")
    );
    repl.insert(
        opt_placeholder,
        config
            .formula_opt_path(formula.name())
            .to_string_lossy()
            .into(),
    );

    match formula.dependencies() {
        Ok(deps) => {
            let llvm_dep_name = deps
                .iter()
                .find(|d| d.name.starts_with("llvm"))
                .map(|d| d.name.clone());
            if let Some(name) = llvm_dep_name {
                let llvm_opt_path = config.formula_opt_path(&name);
                let llvm_lib = llvm_opt_path.join("lib");
                if llvm_lib.is_dir() {
                    repl.insert(
                        "@loader_path/../lib".into(),
                        llvm_lib.to_string_lossy().into(),
                    );
                    repl.insert(
                        format!(
                            "@@HOMEBREW_OPT_{}@@/lib",
                            name.to_uppercase().replace(['-', '+', '.'], "_")
                        ),
                        llvm_lib.to_string_lossy().into(),
                    );
                }
            }
        }
        Err(e) => {
            warn!(
                "Could not check formula dependencies during LLVM relocation for {}: {}",
                formula.name(),
                e
            );
        }
    }

    tracing::debug!("Relocation table:");
    for (k, v) in &repl {
        tracing::debug!("{}  →  {}", k, v);
    }
    original_relocation_scan_and_patch(formula, install_dir, config, repl)
}

fn original_relocation_scan_and_patch(
    _formula: &Formula,
    install_dir: &Path,
    _config: &Config,
    replacements: HashMap<String, String>,
) -> Result<()> {
    let mut text_replaced_count = 0;
    let mut macho_patched_count = 0;
    let mut permission_errors = 0;
    let mut macho_errors = 0;
    let mut io_errors = 0;
    let mut files_to_chmod: Vec<PathBuf> = Vec::new();
    for entry in WalkDir::new(install_dir).into_iter().filter_map(|e| e.ok()) {
        let path = entry.path();
        let file_type = entry.file_type();
        if path
            .components()
            .any(|c| c.as_os_str().to_string_lossy().ends_with(".app"))
        {
            if file_type.is_file() {
                debug!("Skipping relocation inside .app bundle: {}", path.display());
            }
            continue;
        }
        if file_type.is_symlink() {
            debug!("Checking symlink for potential chmod: {}", path.display());
            if path
                .parent()
                .is_some_and(|p| p.ends_with("bin") || p.ends_with("sbin"))
            {
                files_to_chmod.push(path.to_path_buf());
            }
            continue;
        }
        if !file_type.is_file() {
            continue;
        }
        let (meta, initially_executable) = match fs::metadata(path) {
            Ok(m) => {
                #[cfg(unix)]
                let ie = m.permissions().mode() & 0o111 != 0;
                #[cfg(not(unix))]
                let ie = true;
                (m, ie)
            }
            Err(_e) => {
                debug!("Failed to get metadata for {}: {}", path.display(), _e);
                io_errors += 1;
                continue;
            }
        };
        let is_in_exec_dir = path
            .parent()
            .is_some_and(|p| p.ends_with("bin") || p.ends_with("sbin"));
        if meta.permissions().readonly() {
            #[cfg(unix)]
            {
                let mut perms = meta.permissions();
                let current_mode = perms.mode();
                let new_mode = current_mode | 0o200;
                if new_mode != current_mode {
                    perms.set_mode(new_mode);
                    if fs::set_permissions(path, perms).is_err() {
                        debug!(
                            "Skipping readonly file (and couldn't make writable): {}",
                            path.display()
                        );
                        continue;
                    } else {
                        debug!("Made readonly file writable: {}", path.display());
                    }
                }
            }
            #[cfg(not(unix))]
            {
                debug!(
                    "Skipping potentially readonly file on non-unix: {}",
                    path.display()
                );
                continue;
            }
        }
        let mut was_modified = false;
        let mut skipped_paths_for_file = Vec::new();
        if cfg!(target_os = "macos")
            && (initially_executable
                || is_in_exec_dir
                || path
                    .extension()
                    .is_some_and(|e| e == "dylib" || e == "so" || e == "bundle"))
        {
            match macho::patch_macho_file(path, &replacements) {
                Ok((true, skipped_paths)) => {
                    macho_patched_count += 1;
                    was_modified = true;
                    skipped_paths_for_file = skipped_paths;
                }
                Ok((false, skipped_paths)) => {
                    // Not Mach-O or no patches needed, but might have skipped paths
                    skipped_paths_for_file = skipped_paths;
                }
                Err(SpsError::PathTooLongError(e)) => { // Specifically catch path too long
                    error!(
                        "Mach-O patch failed (path too long) for {}: {}",
                        path.display(),
                        e
                    );
                    macho_errors += 1;
                    // Continue scanning other files even if one fails this way
                    continue;
                }
                Err(SpsError::CodesignError(e)) => { // Specifically catch codesign errors
                     error!(
                        "Mach-O patch failed (codesign error) for {}: {}",
                        path.display(),
                        e
                     );
                     macho_errors += 1;
                     // Continue scanning other files
                     continue;
                }
                 // Catch generic MachOError or Object error, treat as non-fatal for text replace
                Err(e @ SpsError::MachOError(_))
                | Err(e @ SpsError::Object(_))
                | Err(e @ SpsError::Generic(_)) // Catch Generic errors from patch_macho too
                | Err(e @ SpsError::Io(_)) => { // Catch IO errors from patch_macho
                    debug!(
                        "Mach-O processing/patching failed for {}: {}. Skipping Mach-O patch for this file.",
                        path.display(),
                        e
                    );
                     // Don't increment macho_errors here, as we fallback to text replace
                     io_errors += 1; // Count as IO or generic error instead
                }
                 // Catch other specific errors if needed
                 Err(e) => {
                      debug!(
                         "Unexpected error during Mach-O check/patch for {}: {}. Falling back to text replacer.",
                         path.display(),
                         e
                      );
                      io_errors += 1;
                 }
            }

            // Handle paths that were too long for Mach-O patching with install_name_tool fallback
            if !skipped_paths_for_file.is_empty() {
                debug!(
                    "Applying install_name_tool fallback for {} skipped paths in {}",
                    skipped_paths_for_file.len(),
                    path.display()
                );
                debug!(
                    "Applying install_name_tool fallback for {} skipped paths in {}",
                    skipped_paths_for_file.len(),
                    path.display()
                );
                for skipped in &skipped_paths_for_file {
                    match apply_install_name_tool_change(&skipped.old_path, &skipped.new_path, path)
                    {
                        Ok(()) => {
                            debug!(
                                "Successfully applied install_name_tool fallback: '{}' -> '{}' in {}",
                                skipped.old_path, skipped.new_path, path.display()
                            );
                            was_modified = true;
                        }
                        Err(e) => {
                            warn!(
                                "install_name_tool fallback failed for '{}' -> '{}' in {}: {}",
                                skipped.old_path,
                                skipped.new_path,
                                path.display(),
                                e
                            );
                            macho_errors += 1;
                        }
                    }
                }
            }
        }
        // Fallback to text replacement if not modified by Mach-O patching
        if !was_modified {
            // Heuristic check for text file (avoid reading huge binaries)
            let mut is_likely_text = false;
            if meta.len() < 5 * 1024 * 1024 {
                if let Ok(mut f) = File::open(path) {
                    let mut buf = [0; 1024];
                    match f.read(&mut buf) {
                        Ok(n) if n > 0 => {
                            if !buf[..n].contains(&0) {
                                is_likely_text = true;
                            }
                        }
                        Ok(_) => {
                            is_likely_text = true;
                        }
                        Err(_) => {}
                    }
                }
            }

            if is_likely_text {
                // Read the file content as string
                if let Ok(content) = fs::read_to_string(path) {
                    let mut new_content = content.clone();
                    let mut replacements_made = false;
                    for (placeholder, replacement) in &replacements {
                        if new_content.contains(placeholder) {
                            new_content = new_content.replace(placeholder, replacement);
                            replacements_made = true;
                        }
                    }
                    // Write back only if changes were made
                    if replacements_made {
                        match write_text_file_atomic(path, &new_content) {
                            Ok(_) => {
                                text_replaced_count += 1;
                                was_modified = true; // Mark as modified for chmod check
                            }
                            Err(e) => {
                                error!(
                                    "Failed to write replaced text to {}: {}",
                                    path.display(),
                                    e
                                );
                                io_errors += 1;
                            }
                        }
                    }
                } else if meta.len() > 0 {
                    debug!(
                        "Could not read {} as string for text replacement.",
                        path.display()
                    );
                    io_errors += 1;
                }
            } else if meta.len() >= 5 * 1024 * 1024 {
                debug!(
                    "Skipping text replacement for large file: {}",
                    path.display()
                );
            } else {
                debug!(
                    "Skipping text replacement for likely binary file: {}",
                    path.display()
                );
            }
        }
        if was_modified || initially_executable || is_in_exec_dir {
            files_to_chmod.push(path.to_path_buf());
        }
    }

    #[cfg(unix)]
    {
        debug!(
            "Ensuring execute permissions for {} potentially executable files/links",
            files_to_chmod.len()
        );
        let unique_files_to_chmod: HashSet<_> = files_to_chmod.into_iter().collect();
        for p in &unique_files_to_chmod {
            if !p.exists() && p.symlink_metadata().is_err() {
                debug!("Skipping chmod for non-existent path: {}", p.display());
                continue;
            }
            match fs::symlink_metadata(p) {
                Ok(m) => {
                    if m.is_file() {
                        let mut perms = m.permissions();
                        let current_mode = perms.mode();
                        let new_mode = current_mode | 0o111;
                        if new_mode != current_mode {
                            perms.set_mode(new_mode);
                            if let Err(e) = fs::set_permissions(p, perms) {
                                debug!("Failed to set +x on {}: {}", p.display(), e);
                                permission_errors += 1;
                            }
                        }
                    }
                }
                Err(e) => {
                    debug!(
                        "Could not stat {} during final chmod pass: {}",
                        p.display(),
                        e
                    );
                    permission_errors += 1;
                }
            }
        }
    }

    debug!(
        "Relocation scan complete. Text files replaced: {}, Mach-O files patched: {}",
        text_replaced_count, macho_patched_count
    );
    if permission_errors > 0 || macho_errors > 0 || io_errors > 0 {
        debug!(
            "Bottle relocation finished with issues: {} chmod errors, {} Mach-O errors, {} IO errors in {}.",
            permission_errors,
            macho_errors,
            io_errors,
            install_dir.display()
        );
        if macho_errors > 0 {
            return Err(SpsError::InstallError(format!(
                "Bottle relocation failed due to {} Mach-O errors in {}",
                macho_errors,
                install_dir.display()
            )));
        }
    }
    Ok(())
}

fn codesign_path(target: &Path) -> Result<()> {
    debug!("Re‑signing: {}", target.display());
    let status = StdCommand::new("codesign")
        .args([
            "-s",
            "-",
            "--force",
            "--preserve-metadata=identifier,entitlements",
            target
                .to_str()
                .ok_or_else(|| SpsError::Generic("Non‑UTF8 path for codesign".into()))?,
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map_err(|e| SpsError::Io(Arc::new(e)))?;
    if !status.success() {
        return Err(SpsError::CodesignError(format!(
            "codesign failed for {}",
            target.display()
        )));
    }
    Ok(())
}
fn write_text_file_atomic(original_path: &Path, content: &str) -> Result<()> {
    let dir = original_path.parent().ok_or_else(|| {
        SpsError::Generic(format!(
            "Cannot get parent directory for {}",
            original_path.display()
        ))
    })?;
    // Ensure the directory exists
    fs::create_dir_all(dir).map_err(|e| SpsError::Io(Arc::new(e)))?; // Use Arc::new

    let mut temp_file = NamedTempFile::new_in(dir)?;
    let temp_path = temp_file.path().to_path_buf(); // Store path before consuming temp_file

    // Write content
    temp_file.write_all(content.as_bytes())?;
    // Ensure data is flushed from application buffer to OS buffer
    temp_file.flush()?;
    // Attempt to sync data from OS buffer to disk (best effort)
    let _ = temp_file.as_file().sync_all();

    // Try to preserve original permissions
    let original_perms = fs::metadata(original_path).map(|m| m.permissions()).ok();

    // Atomically replace the original file with the temporary file
    temp_file.persist(original_path).map_err(|e| {
        error!(
            "Failed to persist/rename temporary text file {} over {}: {}",
            temp_path.display(), // Use stored path for logging
            original_path.display(),
            e.error // Log the underlying IO error
        );
        // Return the IO error wrapped in our error type
        SpsError::Io(Arc::new(e.error)) // Use Arc::new
    })?;

    // Restore original permissions if we captured them
    if let Some(perms) = original_perms {
        // Ignore errors setting permissions, best effort
        let _ = fs::set_permissions(original_path, perms);
    }
    Ok(())
}

#[cfg(unix)]
fn find_brewed_perl(prefix: &Path) -> Option<PathBuf> {
    let opt_dir = prefix.join("opt");
    if !opt_dir.is_dir() {
        return None;
    }
    let mut best: Option<(semver::Version, PathBuf)> = None;
    match fs::read_dir(opt_dir) {
        Ok(entries) => {
            for entry_res in entries.flatten() {
                let name = entry_res.file_name();
                let s = name.to_string_lossy();
                let entry_path = entry_res.path();
                if !entry_path.is_dir() {
                    continue;
                }
                if let Some(version_part) = s.strip_prefix("perl@") {
                    let version_str_padded = if version_part.contains('.') {
                        let parts: Vec<&str> = version_part.split('.').collect();
                        match parts.len() {
                            1 => format!("{}.0.0", parts[0]), // e.g., perl@5 -> 5.0.0
                            2 => format!("{}.{}.0", parts[0], parts[1]), /* e.g., perl@5.30 -> */
                            // 5.30.0
                            _ => version_part.to_string(), // Already 3+ parts
                        }
                    } else {
                        format!("{version_part}.0.0") // e.g., perl@5 -> 5.0.0 (handles case with no
                                                      // dot)
                    };

                    if let Ok(v) = semver::Version::parse(&version_str_padded) {
                        let candidate_bin = entry_path.join("bin/perl");
                        if candidate_bin.is_file()
                            && (best.is_none() || v > best.as_ref().unwrap().0)
                        {
                            best = Some((v, candidate_bin));
                        }
                    }
                } else if s == "perl" {
                    let candidate_bin = entry_path.join("bin/perl");
                    if candidate_bin.is_file() && best.is_none() {
                        if let Ok(v_base) = semver::Version::parse("5.0.0") {
                            best = Some((v_base, candidate_bin));
                        }
                    }
                }
            }
        }
        Err(_e) => {
            debug!("Failed to read opt directory during perl search: {}", _e);
        }
    }
    best.map(|(_, path)| path)
}

#[cfg(not(unix))]
fn find_brewed_perl(_prefix: &Path) -> Option<PathBuf> {
    None
}
fn ensure_llvm_symlinks(install_dir: &Path, formula: &Formula, config: &Config) -> Result<()> {
    let lib_dir = install_dir.join("lib");
    if !lib_dir.exists() {
        debug!(
            "Skipping LLVM symlink creation as lib dir is missing in {}",
            install_dir.display()
        );
        return Ok(());
    }

    let llvm_dep_name = match formula.dependencies() {
        Ok(deps) => deps
            .iter()
            .find(|d| d.name.starts_with("llvm"))
            .map(|dep| dep.name.clone()),
        Err(e) => {
            warn!(
                "Could not check formula dependencies for LLVM symlink creation in {}: {}",
                formula.name(),
                e
            );
            return Ok(()); // Don't error, just skip
        }
    };

    // Proceed only if llvm_dep_name is Some
    let llvm_dep_name = match llvm_dep_name {
        Some(name) => name,
        None => {
            debug!(
                "Formula {} does not list an LLVM dependency.",
                formula.name()
            );
            return Ok(());
        }
    };

    let llvm_opt_path = config.formula_opt_path(&llvm_dep_name);
    let llvm_lib_filename = if cfg!(target_os = "macos") {
        "libLLVM.dylib"
    } else if cfg!(target_os = "linux") {
        "libLLVM.so"
    } else {
        warn!("LLVM library filename unknown for target OS, skipping symlink.");
        return Ok(());
    };
    let llvm_lib_path_in_opt = llvm_opt_path.join("lib").join(llvm_lib_filename);
    if !llvm_lib_path_in_opt.exists() {
        debug!(
            "Required LLVM library not found at {}. Cannot create symlink in {}.",
            llvm_lib_path_in_opt.display(),
            formula.name()
        );
        return Ok(());
    }
    let symlink_target_path = lib_dir.join(llvm_lib_filename);
    if symlink_target_path.exists() || symlink_target_path.symlink_metadata().is_ok() {
        debug!(
            "Symlink or file already exists at {}. Skipping creation.",
            symlink_target_path.display()
        );
        return Ok(());
    }
    #[cfg(unix)]
    {
        if let Some(parent) = symlink_target_path.parent() {
            if !parent.exists() {
                fs::create_dir_all(parent)?;
            }
        }
        match symlink(&llvm_lib_path_in_opt, &symlink_target_path) {
            Ok(_) => debug!(
                "Created symlink {} -> {}",
                symlink_target_path.display(),
                llvm_lib_path_in_opt.display()
            ),
            Err(e) => {
                // Log as warning, don't fail install
                warn!(
                    "Failed to create LLVM symlink {} -> {}: {}",
                    symlink_target_path.display(),
                    llvm_lib_path_in_opt.display(),
                    e
                );
            }
        }

        let rustlib_dir = install_dir.join("lib").join("rustlib");
        if rustlib_dir.is_dir() {
            if let Ok(entries) = fs::read_dir(&rustlib_dir) {
                for entry in entries.flatten() {
                    let triple_path = entry.path();
                    if triple_path.is_dir() {
                        let triple_lib_dir = triple_path.join("lib");
                        if triple_lib_dir.is_dir() {
                            let nested_symlink = triple_lib_dir.join(llvm_lib_filename);

                            if nested_symlink.exists() || nested_symlink.symlink_metadata().is_ok()
                            {
                                debug!(
                                    "Symlink or file already exists at {}, skipping.",
                                    nested_symlink.display()
                                );
                                continue;
                            }

                            if let Some(parent) = nested_symlink.parent() {
                                let _ = fs::create_dir_all(parent);
                            }
                            match symlink(&llvm_lib_path_in_opt, &nested_symlink) {
                                Ok(_) => debug!(
                                    "Created symlink {} -> {}",
                                    nested_symlink.display(),
                                    llvm_lib_path_in_opt.display()
                                ),
                                Err(e) => warn!(
                                    "Failed to create LLVM symlink {} -> {}: {}",
                                    nested_symlink.display(),
                                    llvm_lib_path_in_opt.display(),
                                    e
                                ),
                            }
                        }
                    }
                }
            }
        }
    }
    Ok(())
}

/// Applies a path change using install_name_tool as a fallback for Mach-O files
/// where the path replacement is too long for direct binary patching.
fn apply_install_name_tool_change(old_path: &str, new_path: &str, target: &Path) -> Result<()> {
    if !target.exists() {
        debug!(
            "Target {} does not exist, skipping install_name_tool fallback.",
            target.display()
        );
        return Ok(());
    }

    debug!(
        "Running install_name_tool -change {} {} {}",
        old_path,
        new_path,
        target.display()
    );

    let output = StdCommand::new("install_name_tool")
        .args(["-change", old_path, new_path, target.to_str().unwrap()])
        .output()
        .map_err(|e| SpsError::Io(Arc::new(e)))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        if !stderr.contains("file not found")
            && !stderr.contains("no LC_LOAD_DYLIB command specifying file")
            && !stderr.contains("is not a Mach-O file")
            && !stderr.contains("object file format invalid")
            && !stderr.trim().is_empty()
        {
            error!(
                "install_name_tool -change failed unexpectedly for target {}: {}",
                target.display(),
                stderr.trim()
            );
            return Err(SpsError::InstallError(format!(
                "install_name_tool failed for {}: {}",
                target.display(),
                stderr.trim()
            )));
        } else {
            debug!(
                "install_name_tool -change: old path '{}' likely not found or target '{}' not relevant (stderr: {}).",
                old_path,
                target.display(),
                stderr.trim()
            );
            return Ok(()); // No changes made, skip re-signing
        }
    }

    // Re-sign the binary after making changes (required on Apple Silicon)
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    {
        debug!(
            "Re-signing binary after install_name_tool change: {}",
            target.display()
        );
        codesign_path(target)?;
    }

    debug!(
        "Successfully applied install_name_tool fallback for {} -> {} in {}",
        old_path,
        new_path,
        target.display()
    );

    Ok(())
}

// sps/src/pipeline/planner.rs
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::Arc;

use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::dependency::resolver::{
    DependencyResolver, NodeInstallStrategy, PerTargetInstallPreferences, ResolutionContext,
    ResolutionStatus, ResolvedGraph,
};
use sps_common::error::{Result as SpsResult, SpsError};
use sps_common::formulary::Formulary;
use sps_common::keg::KegRegistry;
use sps_common::model::{Cask, Formula, InstallTargetIdentifier};
use sps_common::pipeline::{JobAction, PipelineEvent, PlannedJob, PlannedOperations};
use sps_core::check::installed::{self, InstalledPackageInfo, PackageType as CorePackageType};
use sps_core::check::update::{self, UpdateInfo};
use tokio::sync::broadcast;
use tokio::task::JoinSet;
use tracing::{debug, error as trace_error, instrument, warn};

use super::runner::{get_panic_message, CommandType, PipelineFlags};

pub(crate) type PlanResult<T> = SpsResult<T>;

#[derive(Debug, Default)]
struct IntermediatePlan {
    initial_ops: HashMap<String, (JobAction, Option<InstallTargetIdentifier>)>,
    errors: Vec<(String, SpsError)>,
    already_satisfied: HashSet<String>,
    processed_globally: HashSet<String>,
    private_store_sources: HashMap<String, PathBuf>,
}

#[instrument(skip(cache))]
pub(crate) async fn fetch_target_definitions(
    names: &[String],
    cache: Arc<Cache>,
) -> HashMap<String, SpsResult<InstallTargetIdentifier>> {
    let mut results = HashMap::new();
    if names.is_empty() {
        return results;
    }
    let mut futures = JoinSet::new();

    let formulae_map_handle = tokio::spawn(load_or_fetch_formulae_map(cache.clone()));
    let casks_map_handle = tokio::spawn(load_or_fetch_casks_map(cache.clone()));

    let formulae_map = match formulae_map_handle.await {
        Ok(Ok(map)) => Some(map),
        Ok(Err(e)) => {
            debug!("[FetchDefs] Failed to load/fetch full formulae list: {}", e);
            None
        }
        Err(e) => {
            warn!(
                "[FetchDefs] Formulae map loading task panicked: {}",
                get_panic_message(e.into_panic()) // Ensure get_panic_message is accessible
            );
            None
        }
    };
    let casks_map = match casks_map_handle.await {
        Ok(Ok(map)) => Some(map),
        Ok(Err(e)) => {
            debug!("[FetchDefs] Failed to load/fetch full casks list: {}", e);
            None
        }
        Err(e) => {
            warn!(
                "[FetchDefs] Casks map loading task panicked: {}",
                get_panic_message(e.into_panic()) // Ensure get_panic_message is accessible
            );
            None
        }
    };

    for name_str in names {
        let name_owned = name_str.to_string();
        let local_formulae_map = formulae_map.clone();
        let local_casks_map = casks_map.clone();

        futures.spawn(async move {
            if let Some(ref map) = local_formulae_map {
                if let Some(f_arc) = map.get(&name_owned) {
                    return (name_owned, Ok(InstallTargetIdentifier::Formula(f_arc.clone())));
                }
            }
            if let Some(ref map) = local_casks_map {
                if let Some(c_arc) = map.get(&name_owned) {
                    return (name_owned, Ok(InstallTargetIdentifier::Cask(c_arc.clone())));
                }
            }
            debug!("[FetchDefs] Definition for '{}' not found in cached lists, fetching directly from API...", name_owned);
            match sps_net::api::get_formula(&name_owned).await {
                Ok(formula_obj) => return (name_owned, Ok(InstallTargetIdentifier::Formula(Arc::new(formula_obj)))),
                Err(SpsError::NotFound(_)) => {}
                Err(e) => return (name_owned, Err(e)),
            }
            match sps_net::api::get_cask(&name_owned).await {
                Ok(cask_obj) => (name_owned, Ok(InstallTargetIdentifier::Cask(Arc::new(cask_obj)))),
                Err(SpsError::NotFound(_)) => (name_owned.clone(), Err(SpsError::NotFound(format!("Formula or Cask '{name_owned}' not found")))),
                Err(e) => (name_owned, Err(e)),
            }
        });
    }

    while let Some(res) = futures.join_next().await {
        match res {
            Ok((name, result)) => {
                results.insert(name, result);
            }
            Err(e) => {
                let panic_message = get_panic_message(e.into_panic());
                trace_error!(
                    "[FetchDefs] Task panicked during definition fetch: {}",
                    panic_message
                );
                results.insert(
                    format!("[unknown_target_due_to_panic_{}]", results.len()),
                    Err(SpsError::Generic(format!(
                        "Definition fetching task panicked: {panic_message}"
                    ))),
                );
            }
        }
    }
    results
}

async fn load_or_fetch_formulae_map(cache: Arc<Cache>) -> SpsResult<HashMap<String, Arc<Formula>>> {
    match cache.load_raw("formula.json") {
        Ok(data) => {
            let formulas: Vec<Formula> = serde_json::from_str(&data)
                .map_err(|e| SpsError::Cache(format!("Parse cached formula.json failed: {e}")))?;
            Ok(formulas
                .into_iter()
                .map(|f| (f.name.clone(), Arc::new(f)))
                .collect())
        }
        Err(_) => {
            debug!("[FetchDefs] Cache miss for formula.json, fetching from API...");
            let raw_data = sps_net::api::fetch_all_formulas().await?;
            if let Err(e) = cache.store_raw("formula.json", &raw_data) {
                warn!("Failed to store formula.json in cache: {}", e);
            }
            let formulas: Vec<Formula> =
                serde_json::from_str(&raw_data).map_err(|e| SpsError::Json(Arc::new(e)))?;
            Ok(formulas
                .into_iter()
                .map(|f| (f.name.clone(), Arc::new(f)))
                .collect())
        }
    }
}

async fn load_or_fetch_casks_map(cache: Arc<Cache>) -> SpsResult<HashMap<String, Arc<Cask>>> {
    match cache.load_raw("cask.json") {
        Ok(data) => {
            let casks: Vec<Cask> = serde_json::from_str(&data)
                .map_err(|e| SpsError::Cache(format!("Parse cached cask.json failed: {e}")))?;
            Ok(casks
                .into_iter()
                .map(|c| (c.token.clone(), Arc::new(c)))
                .collect())
        }
        Err(_) => {
            debug!("[FetchDefs] Cache miss for cask.json, fetching from API...");
            let raw_data = sps_net::api::fetch_all_casks().await?;
            if let Err(e) = cache.store_raw("cask.json", &raw_data) {
                warn!("Failed to store cask.json in cache: {}", e);
            }
            let casks: Vec<Cask> =
                serde_json::from_str(&raw_data).map_err(|e| SpsError::Json(Arc::new(e)))?;
            Ok(casks
                .into_iter()
                .map(|c| (c.token.clone(), Arc::new(c)))
                .collect())
        }
    }
}

pub(crate) struct OperationPlanner<'a> {
    config: &'a Config,
    cache: Arc<Cache>,
    flags: &'a PipelineFlags,
    event_tx: broadcast::Sender<PipelineEvent>,
}

impl<'a> OperationPlanner<'a> {
    pub fn new(
        config: &'a Config,
        cache: Arc<Cache>,
        flags: &'a PipelineFlags,
        event_tx: broadcast::Sender<PipelineEvent>,
    ) -> Self {
        Self {
            config,
            cache,
            flags,
            event_tx,
        }
    }

    fn get_previous_installation_type(&self, old_keg_path: &Path) -> Option<String> {
        let receipt_path = old_keg_path.join("INSTALL_RECEIPT.json");
        if !receipt_path.is_file() {
            tracing::debug!(
                "No INSTALL_RECEIPT.json found at {} for previous version.",
                receipt_path.display()
            );
            return None;
        }

        match std::fs::read_to_string(&receipt_path) {
            Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
                Ok(json_value) => {
                    let inst_type = json_value
                        .get("installation_type")
                        .and_then(|it| it.as_str())
                        .map(String::from);
                    tracing::debug!(
                        "Previous installation type for {}: {:?}",
                        old_keg_path.display(),
                        inst_type
                    );
                    inst_type
                }
                Err(e) => {
                    tracing::warn!("Failed to parse INSTALL_RECEIPT.json at {}: {}. Cannot determine previous installation type.", receipt_path.display(), e);
                    None
                }
            },
            Err(e) => {
                tracing::warn!("Failed to read INSTALL_RECEIPT.json at {}: {}. Cannot determine previous installation type.", receipt_path.display(), e);
                None
            }
        }
    }

    async fn check_installed_status(&self, name: &str) -> PlanResult<Option<InstalledPackageInfo>> {
        installed::get_installed_package(name, self.config).await
    }

    async fn determine_cask_private_store_source(
        &self,
        name: &str,
        version_for_path: &str,
    ) -> Option<PathBuf> {
        let cask_def_res = fetch_target_definitions(&[name.to_string()], self.cache.clone())
            .await
            .remove(name);

        if let Some(Ok(InstallTargetIdentifier::Cask(cask_arc))) = cask_def_res {
            if let Some(artifacts) = &cask_arc.artifacts {
                for artifact_entry in artifacts {
                    if let Some(app_array) = artifact_entry.get("app").and_then(|v| v.as_array()) {
                        if let Some(app_name_val) = app_array.first() {
                            if let Some(app_name_str) = app_name_val.as_str() {
                                let private_path = self.config.cask_store_app_path(
                                    name,
                                    version_for_path,
                                    app_name_str,
                                );
                                if private_path.exists() && private_path.is_dir() {
                                    debug!("[Planner] Found reusable Cask private store bundle for {} version {}: {}", name, version_for_path, private_path.display());
                                    return Some(private_path);
                                }
                            }
                        }
                        break;
                    }
                }
            }
        }
        None
    }

    async fn plan_for_install(&self, targets: &[String]) -> PlanResult<IntermediatePlan> {
        let mut plan = IntermediatePlan::default();
        for name in targets {
            if plan.processed_globally.contains(name) {
                continue;
            }
            match self.check_installed_status(name).await {
                Ok(Some(installed_info)) => {
                    let mut proceed_with_install = false;
                    if installed_info.pkg_type == CorePackageType::Cask {
                        let manifest_path = installed_info.path.join("CASK_INSTALL_MANIFEST.json");
                        if manifest_path.is_file() {
                            if let Ok(manifest_str) = std::fs::read_to_string(&manifest_path) {
                                if let Ok(manifest_json) =
                                    serde_json::from_str::<serde_json::Value>(&manifest_str)
                                {
                                    if let Some(is_installed_flag) =
                                        manifest_json.get("is_installed").and_then(|v| v.as_bool())
                                    {
                                        if !is_installed_flag {
                                            debug!("Cask '{}' found but marked as not installed in manifest. Proceeding with install.", name);
                                            proceed_with_install = true;
                                        }
                                    }
                                }
                            }
                        } else {
                            debug!(
                                "Cask '{}' found but manifest missing. Assuming needs install.",
                                name
                            );
                            proceed_with_install = true;
                        }
                    }
                    if proceed_with_install {
                        if let Some(private_path) = self
                            .determine_cask_private_store_source(name, &installed_info.version)
                            .await
                        {
                            plan.private_store_sources
                                .insert(name.clone(), private_path);
                        }
                        plan.initial_ops
                            .insert(name.clone(), (JobAction::Install, None));
                    } else {
                        debug!("Target '{}' already installed and manifest indicates it. Marking as satisfied.", name);
                        plan.already_satisfied.insert(name.clone());
                        plan.processed_globally.insert(name.clone());
                    }
                }
                Ok(None) => {
                    if let Some(private_path) = self
                        .determine_cask_private_store_source(name, "latest")
                        .await
                    {
                        plan.private_store_sources
                            .insert(name.clone(), private_path);
                    }
                    plan.initial_ops
                        .insert(name.clone(), (JobAction::Install, None));
                }
                Err(e) => {
                    plan.errors.push((
                        name.clone(),
                        SpsError::Generic(format!(
                            "Failed to check installed status for {name}: {e}"
                        )),
                    ));
                    plan.processed_globally.insert(name.clone());
                }
            }
        }
        Ok(plan)
    }

    async fn plan_for_reinstall(&self, targets: &[String]) -> PlanResult<IntermediatePlan> {
        let mut plan = IntermediatePlan::default();
        for name in targets {
            if plan.processed_globally.contains(name) {
                continue;
            }
            match self.check_installed_status(name).await {
                Ok(Some(installed_info)) => {
                    if installed_info.pkg_type == CorePackageType::Cask {
                        if let Some(private_path) = self
                            .determine_cask_private_store_source(name, &installed_info.version)
                            .await
                        {
                            plan.private_store_sources
                                .insert(name.clone(), private_path);
                        }
                    }
                    plan.initial_ops.insert(
                        name.clone(),
                        (
                            JobAction::Reinstall {
                                version: installed_info.version.clone(),
                                current_install_path: installed_info.path.clone(),
                            },
                            None,
                        ),
                    );
                }
                Ok(None) => {
                    plan.errors.push((
                        name.clone(),
                        SpsError::NotFound(format!("Cannot reinstall '{name}': not installed.")),
                    ));
                    plan.processed_globally.insert(name.clone());
                }
                Err(e) => {
                    plan.errors.push((
                        name.clone(),
                        SpsError::Generic(format!("Failed to check status for '{name}': {e}")),
                    ));
                    plan.processed_globally.insert(name.clone());
                }
            }
        }
        Ok(plan)
    }

    async fn plan_for_upgrade(
        &self,
        targets: &[String],
        all: bool,
    ) -> PlanResult<IntermediatePlan> {
        let mut plan = IntermediatePlan::default();
        let packages_to_check = if all {
            installed::get_installed_packages(self.config)
                .await
                .map_err(|e| {
                    plan.errors.push((
                        "<all>".to_string(),
                        SpsError::Generic(format!("Failed to get installed packages: {e}")),
                    ));
                    e
                })?
        } else {
            let mut specific = Vec::new();
            for name in targets {
                match self.check_installed_status(name).await {
                    Ok(Some(info)) => {
                        if info.pkg_type == CorePackageType::Cask {
                            let manifest_path = info.path.join("CASK_INSTALL_MANIFEST.json");
                            if manifest_path.is_file() {
                                if let Ok(manifest_str) = std::fs::read_to_string(&manifest_path) {
                                    if let Ok(manifest_json) =
                                        serde_json::from_str::<serde_json::Value>(&manifest_str)
                                    {
                                        if !manifest_json
                                            .get("is_installed")
                                            .and_then(|v| v.as_bool())
                                            .unwrap_or(true)
                                        {
                                            debug!("Skipping upgrade for Cask '{}' as its manifest indicates it's not fully installed.", name);
                                            plan.processed_globally.insert(name.clone());
                                            continue;
                                        }
                                    }
                                }
                            }
                        }
                        specific.push(info);
                    }
                    Ok(None) => {
                        plan.errors.push((
                            name.to_string(),
                            SpsError::NotFound(format!("Cannot upgrade '{name}': not installed.")),
                        ));
                        plan.processed_globally.insert(name.clone());
                    }
                    Err(e) => {
                        plan.errors.push((
                            name.to_string(),
                            SpsError::Generic(format!("Failed to check status for '{name}': {e}")),
                        ));
                        plan.processed_globally.insert(name.clone());
                    }
                }
            }
            specific
        };

        if packages_to_check.is_empty() {
            return Ok(plan);
        }

        match update::check_for_updates(&packages_to_check, &self.cache, self.config).await {
            Ok(updates) => {
                let update_map: HashMap<String, UpdateInfo> =
                    updates.into_iter().map(|u| (u.name.clone(), u)).collect();

                debug!(
                    "[Planner] Found {} available updates out of {} packages checked",
                    update_map.len(),
                    packages_to_check.len()
                );
                debug!(
                    "[Planner] Available updates: {:?}",
                    update_map.keys().collect::<Vec<_>>()
                );

                for p_info in packages_to_check {
                    if plan.processed_globally.contains(&p_info.name) {
                        continue;
                    }
                    if let Some(ui) = update_map.get(&p_info.name) {
                        debug!(
                            "[Planner] Adding upgrade job for '{}': {} -> {}",
                            p_info.name, p_info.version, ui.available_version
                        );
                        plan.initial_ops.insert(
                            p_info.name.clone(),
                            (
                                JobAction::Upgrade {
                                    from_version: p_info.version.clone(),
                                    old_install_path: p_info.path.clone(),
                                },
                                Some(ui.target_definition.clone()),
                            ),
                        );
                        // Don't mark packages with updates as processed_globally
                        // so they can be included in the final job list
                    } else {
                        debug!(
                            "[Planner] No update available for '{}', marking as already satisfied",
                            p_info.name
                        );
                        plan.already_satisfied.insert(p_info.name.clone());
                        // Only mark packages without updates as processed_globally
                        plan.processed_globally.insert(p_info.name.clone());
                    }
                }
            }
            Err(e) => {
                plan.errors.push((
                    "[Update Check]".to_string(),
                    SpsError::Generic(format!("Failed to check for updates: {e}")),
                ));
            }
        }
        Ok(plan)
    }

    // This now returns sps_common::pipeline::PlannedOperations
    pub async fn plan_operations(
        &self,
        initial_targets: &[String],
        command_type: CommandType,
    ) -> PlanResult<PlannedOperations> {
        debug!(
            "[Planner] Starting plan_operations with command_type: {:?}, targets: {:?}",
            command_type, initial_targets
        );

        let mut intermediate_plan = match command_type {
            CommandType::Install => self.plan_for_install(initial_targets).await?,
            CommandType::Reinstall => self.plan_for_reinstall(initial_targets).await?,
            CommandType::Upgrade { all } => {
                debug!("[Planner] Calling plan_for_upgrade with all={}", all);
                let plan = self.plan_for_upgrade(initial_targets, all).await?;
                debug!("[Planner] plan_for_upgrade returned with {} initial_ops, {} errors, {} already_satisfied",
                    plan.initial_ops.len(), plan.errors.len(), plan.already_satisfied.len());
                debug!(
                    "[Planner] Initial ops: {:?}",
                    plan.initial_ops.keys().collect::<Vec<_>>()
                );
                debug!("[Planner] Already satisfied: {:?}", plan.already_satisfied);
                plan
            }
        };

        let definitions_to_fetch: Vec<String> = intermediate_plan
            .initial_ops
            .iter()
            .filter(|(name, (_, opt_def))| {
                opt_def.is_none() && !intermediate_plan.processed_globally.contains(*name)
            })
            .map(|(name, _)| name.clone())
            .collect();

        if !definitions_to_fetch.is_empty() {
            let fetched_defs =
                fetch_target_definitions(&definitions_to_fetch, self.cache.clone()).await;
            for (name, result) in fetched_defs {
                match result {
                    Ok(target_def) => {
                        if let Some((_action, opt_install_target)) =
                            intermediate_plan.initial_ops.get_mut(&name)
                        {
                            *opt_install_target = Some(target_def);
                        }
                    }
                    Err(e) => {
                        intermediate_plan.errors.push((
                            name.clone(),
                            SpsError::Generic(format!("Failed to get definition for target: {e}")),
                        ));
                        intermediate_plan.processed_globally.insert(name);
                    }
                }
            }
        }
        self.event_tx
            .send(PipelineEvent::DependencyResolutionStarted)
            .ok();

        let mut formulae_for_resolution: HashMap<String, InstallTargetIdentifier> = HashMap::new();
        let mut cask_deps_map: HashMap<String, Arc<Cask>> = HashMap::new();
        let mut cask_processing_queue: VecDeque<String> = VecDeque::new();

        for (name, (action, opt_def)) in &intermediate_plan.initial_ops {
            if intermediate_plan.processed_globally.contains(name) {
                continue;
            }

            // Handle both normal formula targets and upgrade targets
            match opt_def {
                Some(target @ InstallTargetIdentifier::Formula(_)) => {
                    debug!(
                        "[Planner] Adding formula '{}' to resolution list with action {:?}",
                        name, action
                    );
                    formulae_for_resolution.insert(name.clone(), target.clone());
                }
                Some(InstallTargetIdentifier::Cask(c_arc)) => {
                    debug!("[Planner] Adding cask '{}' to processing queue", name);
                    cask_processing_queue.push_back(name.clone());
                    cask_deps_map.insert(name.clone(), c_arc.clone());
                }
                None => {
                    if !intermediate_plan
                        .errors
                        .iter()
                        .any(|(err_name, _)| err_name == name)
                    {
                        intermediate_plan.errors.push((
                            name.clone(),
                            SpsError::Generic(format!(
                                "Definition for '{name}' still missing after fetch attempt."
                            )),
                        ));
                        intermediate_plan.processed_globally.insert(name.clone());
                    }
                }
            }
        }

        let mut processed_casks_for_deps_pass: HashSet<String> =
            intermediate_plan.processed_globally.clone();

        while let Some(cask_token) = cask_processing_queue.pop_front() {
            if processed_casks_for_deps_pass.contains(&cask_token) {
                continue;
            }
            processed_casks_for_deps_pass.insert(cask_token.clone());

            let cask_arc = match cask_deps_map.get(&cask_token) {
                Some(c) => c.clone(),
                None => {
                    match fetch_target_definitions(
                        std::slice::from_ref(&cask_token),
                        self.cache.clone(),
                    )
                    .await
                    .remove(&cask_token)
                    {
                        Some(Ok(InstallTargetIdentifier::Cask(c))) => {
                            cask_deps_map.insert(cask_token.clone(), c.clone());
                            c
                        }
                        Some(Err(e)) => {
                            intermediate_plan.errors.push((cask_token.clone(), e));
                            intermediate_plan
                                .processed_globally
                                .insert(cask_token.clone());
                            continue;
                        }
                        _ => {
                            intermediate_plan.errors.push((
                                cask_token.clone(),
                                SpsError::NotFound(format!(
                                    "Cask definition for dependency '{cask_token}' not found."
                                )),
                            ));
                            intermediate_plan
                                .processed_globally
                                .insert(cask_token.clone());
                            continue;
                        }
                    }
                }
            };

            if let Some(deps) = &cask_arc.depends_on {
                for formula_dep_name in &deps.formula {
                    if formulae_for_resolution.contains_key(formula_dep_name)
                        || intermediate_plan
                            .errors
                            .iter()
                            .any(|(n, _)| n == formula_dep_name)
                        || intermediate_plan
                            .already_satisfied
                            .contains(formula_dep_name)
                    {
                        continue;
                    }
                    match fetch_target_definitions(
                        std::slice::from_ref(formula_dep_name),
                        self.cache.clone(),
                    )
                    .await
                    .remove(formula_dep_name)
                    {
                        Some(Ok(target_def @ InstallTargetIdentifier::Formula(_))) => {
                            formulae_for_resolution.insert(formula_dep_name.clone(), target_def);
                        }
                        Some(Ok(InstallTargetIdentifier::Cask(_))) => {
                            intermediate_plan.errors.push((
                                formula_dep_name.clone(),
                                SpsError::Generic(format!(
                                    "Dependency '{formula_dep_name}' of Cask '{cask_token}' is unexpectedly a Cask itself."
                                )),
                            ));
                            intermediate_plan
                                .processed_globally
                                .insert(formula_dep_name.clone());
                        }
                        Some(Err(e)) => {
                            intermediate_plan.errors.push((
                                formula_dep_name.clone(),
                                SpsError::Generic(format!(
                                    "Failed def fetch for formula dep '{formula_dep_name}' of cask '{cask_token}': {e}"
                                )),
                            ));
                            intermediate_plan
                                .processed_globally
                                .insert(formula_dep_name.clone());
                        }
                        None => {
                            intermediate_plan.errors.push((
                                formula_dep_name.clone(),
                                SpsError::NotFound(format!(
                                    "Formula dep '{formula_dep_name}' for cask '{cask_token}' not found."
                                )),
                            ));
                            intermediate_plan
                                .processed_globally
                                .insert(formula_dep_name.clone());
                        }
                    }
                }
                for dep_cask_token in &deps.cask {
                    if !processed_casks_for_deps_pass.contains(dep_cask_token)
                        && !cask_processing_queue.contains(dep_cask_token)
                    {
                        cask_processing_queue.push_back(dep_cask_token.clone());
                    }
                }
            }
        }

        let mut resolved_formula_graph_opt: Option<Arc<ResolvedGraph>> = None;
        if !formulae_for_resolution.is_empty() {
            let targets_for_resolver: Vec<_> = formulae_for_resolution.keys().cloned().collect();
            let formulary = Formulary::new(self.config.clone());
            let keg_registry = KegRegistry::new(self.config.clone());

            let per_target_prefs = PerTargetInstallPreferences {
                force_source_build_targets: if self.flags.build_from_source {
                    targets_for_resolver.iter().cloned().collect()
                } else {
                    HashSet::new()
                },
                force_bottle_only_targets: HashSet::new(),
            };

            // Create map of initial target actions for the resolver
            let initial_target_actions: HashMap<String, JobAction> = intermediate_plan
                .initial_ops
                .iter()
                .filter_map(|(name, (action, _))| {
                    if targets_for_resolver.contains(name) {
                        Some((name.clone(), action.clone()))
                    } else {
                        debug!("[Planner] WARNING: Target '{}' with action {:?} is not in targets_for_resolver!", name, action);
                        None
                    }
                })
                .collect();

            debug!(
                "[Planner] Created initial_target_actions map with {} entries: {:?}",
                initial_target_actions.len(),
                initial_target_actions
            );
            debug!("[Planner] Targets for resolver: {:?}", targets_for_resolver);

            let ctx = ResolutionContext {
                formulary: &formulary,
                keg_registry: &keg_registry,
                sps_prefix: self.config.sps_root(),
                include_optional: self.flags.include_optional,
                include_test: false,
                skip_recommended: self.flags.skip_recommended,
                initial_target_preferences: &per_target_prefs,
                build_all_from_source: self.flags.build_from_source,
                cascade_source_preference_to_dependencies: true,
                has_bottle_for_current_platform:
                    sps_core::install::bottle::has_bottle_for_current_platform,
                initial_target_actions: &initial_target_actions,
            };

            let mut resolver = DependencyResolver::new(ctx);
            debug!("[Planner] Created DependencyResolver, calling resolve_targets...");
            match resolver.resolve_targets(&targets_for_resolver) {
                Ok(g) => {
                    debug!(
                        "[Planner] Dependency resolution succeeded! Install plan has {} items",
                        g.install_plan.len()
                    );
                    resolved_formula_graph_opt = Some(Arc::new(g));
                }
                Err(e) => {
                    debug!("[Planner] Dependency resolution failed: {}", e);
                    let resolver_error_msg = e.to_string(); // Capture full error
                    for n in targets_for_resolver {
                        if !intermediate_plan
                            .errors
                            .iter()
                            .any(|(err_n, _)| err_n == &n)
                        {
                            intermediate_plan.errors.push((
                                n.clone(),
                                SpsError::DependencyError(resolver_error_msg.clone()),
                            ));
                        }
                        intermediate_plan.processed_globally.insert(n);
                    }
                }
            }
        }
        self.event_tx
            .send(PipelineEvent::DependencyResolutionFinished)
            .ok();

        let mut final_planned_jobs: Vec<PlannedJob> = Vec::new();
        let mut names_processed_from_initial_ops = HashSet::new();

        debug!(
            "[Planner] Processing {} initial_ops into final jobs",
            intermediate_plan.initial_ops.len()
        );
        for (name, (action, opt_def)) in &intermediate_plan.initial_ops {
            debug!(
                "[Planner] Processing initial op '{}': action={:?}, has_def={}",
                name,
                action,
                opt_def.is_some()
            );

            if intermediate_plan.processed_globally.contains(name) {
                debug!("[Planner] Skipping '{}' - already processed globally", name);
                continue;
            }
            // If an error was recorded for this specific initial target (e.g. resolver failed for
            // it, or def missing) ensure it's marked as globally processed and not
            // added to final_planned_jobs.
            if intermediate_plan
                .errors
                .iter()
                .any(|(err_name, _)| err_name == name)
            {
                debug!("[Planner] Skipping job for initial op '{}' as an error was recorded for it during planning.", name);
                intermediate_plan.processed_globally.insert(name.clone());
                continue;
            }
            if intermediate_plan.already_satisfied.contains(name) {
                debug!(
                    "[Planner] Skipping job for initial op '{}' as it's already satisfied.",
                    name
                );
                intermediate_plan.processed_globally.insert(name.clone());
                continue;
            }

            match opt_def {
                Some(target_def) => {
                    let is_source_build = determine_build_strategy_for_job(
                        target_def,
                        action,
                        self.flags,
                        resolved_formula_graph_opt.as_deref(),
                        self,
                    );

                    final_planned_jobs.push(PlannedJob {
                        target_id: name.clone(),
                        target_definition: target_def.clone(),
                        action: action.clone(),
                        is_source_build,
                        use_private_store_source: intermediate_plan
                            .private_store_sources
                            .get(name)
                            .cloned(),
                    });
                    names_processed_from_initial_ops.insert(name.clone());
                }
                None => {
                    tracing::error!("[Planner] CRITICAL: Definition missing for planned operation on '{}' but no error was recorded in intermediate_plan.errors. This should not happen.", name);
                    if !intermediate_plan
                        .errors
                        .iter()
                        .any(|(err_n, _)| err_n == name)
                    {
                        intermediate_plan.errors.push((
                            name.clone(),
                            SpsError::Generic("Definition missing unexpectedly.".into()),
                        ));
                    }
                    intermediate_plan.processed_globally.insert(name.clone());
                }
            }
        }

        if let Some(graph) = resolved_formula_graph_opt.as_ref() {
            for dep_detail in &graph.install_plan {
                let dep_name = dep_detail.formula.name();

                if names_processed_from_initial_ops.contains(dep_name)
                    || intermediate_plan.processed_globally.contains(dep_name)
                    || final_planned_jobs.iter().any(|j| j.target_id == dep_name)
                {
                    continue;
                }

                if intermediate_plan
                    .errors
                    .iter()
                    .any(|(err_name, _)| err_name == dep_name)
                {
                    tracing::debug!("[Planner] Skipping job for dependency '{}' due to a pre-existing error recorded for it.", dep_name);
                    intermediate_plan
                        .processed_globally
                        .insert(dep_name.to_string());
                    continue;
                }
                if dep_detail.status == ResolutionStatus::Failed {
                    tracing::debug!("[Planner] Skipping job for dependency '{}' as its resolution status is Failed. Adding to planner errors.", dep_name);
                    // Ensure this error is also captured if not already.
                    if !intermediate_plan
                        .errors
                        .iter()
                        .any(|(err_name, _)| err_name == dep_name)
                    {
                        intermediate_plan.errors.push((
                            dep_name.to_string(),
                            SpsError::DependencyError(format!(
                                "Resolution failed for dependency {dep_name}"
                            )),
                        ));
                    }
                    intermediate_plan
                        .processed_globally
                        .insert(dep_name.to_string());
                    continue;
                }

                if matches!(
                    dep_detail.status,
                    ResolutionStatus::Missing | ResolutionStatus::Requested
                ) {
                    let is_source_build_for_dep = determine_build_strategy_for_job(
                        &InstallTargetIdentifier::Formula(dep_detail.formula.clone()),
                        &JobAction::Install,
                        self.flags,
                        Some(graph),
                        self,
                    );
                    debug!(
                        "Planning install for new formula dependency '{}'. Source build: {}",
                        dep_name, is_source_build_for_dep
                    );

                    final_planned_jobs.push(PlannedJob {
                        target_id: dep_name.to_string(),
                        target_definition: InstallTargetIdentifier::Formula(
                            dep_detail.formula.clone(),
                        ),
                        action: JobAction::Install,
                        is_source_build: is_source_build_for_dep,
                        use_private_store_source: None,
                    });
                } else if dep_detail.status == ResolutionStatus::Installed {
                    intermediate_plan
                        .already_satisfied
                        .insert(dep_name.to_string());
                }
            }
        }

        for (cask_token, cask_arc) in cask_deps_map {
            if names_processed_from_initial_ops.contains(&cask_token)
                || intermediate_plan.processed_globally.contains(&cask_token)
                || final_planned_jobs.iter().any(|j| j.target_id == cask_token)
            {
                continue;
            }

            match self.check_installed_status(&cask_token).await {
                Ok(None) => {
                    final_planned_jobs.push(PlannedJob {
                        target_id: cask_token.clone(),
                        target_definition: InstallTargetIdentifier::Cask(cask_arc.clone()),
                        action: JobAction::Install,
                        is_source_build: false,
                        use_private_store_source: intermediate_plan
                            .private_store_sources
                            .get(&cask_token)
                            .cloned(),
                    });
                }
                Ok(Some(_installed_info)) => {
                    intermediate_plan
                        .already_satisfied
                        .insert(cask_token.clone());
                }
                Err(e) => {
                    intermediate_plan.errors.push((
                        cask_token.clone(),
                        SpsError::Generic(format!(
                            "Failed check install status for cask dependency {cask_token}: {e}"
                        )),
                    ));
                    intermediate_plan
                        .processed_globally
                        .insert(cask_token.clone());
                }
            }
        }
        if let Some(graph) = resolved_formula_graph_opt.as_ref() {
            if !final_planned_jobs.is_empty() {
                sort_planned_jobs(&mut final_planned_jobs, graph);
            }
        }

        debug!(
            "[Planner] Finishing plan_operations with {} jobs, {} errors, {} already_satisfied",
            final_planned_jobs.len(),
            intermediate_plan.errors.len(),
            intermediate_plan.already_satisfied.len()
        );
        debug!(
            "[Planner] Final jobs: {:?}",
            final_planned_jobs
                .iter()
                .map(|j| &j.target_id)
                .collect::<Vec<_>>()
        );

        Ok(PlannedOperations {
            jobs: final_planned_jobs,
            errors: intermediate_plan.errors,
            already_installed_or_up_to_date: intermediate_plan.already_satisfied,
            resolved_graph: resolved_formula_graph_opt,
        })
    }
}

fn determine_build_strategy_for_job(
    target_def: &InstallTargetIdentifier,
    action: &JobAction,
    flags: &PipelineFlags,
    resolved_graph: Option<&ResolvedGraph>,
    planner: &OperationPlanner,
) -> bool {
    match target_def {
        InstallTargetIdentifier::Formula(formula_arc) => {
            if flags.build_from_source {
                return true;
            }
            if let Some(graph) = resolved_graph {
                if let Some(resolved_detail) = graph.resolution_details.get(formula_arc.name()) {
                    match resolved_detail.determined_install_strategy {
                        NodeInstallStrategy::SourceOnly => return true,
                        NodeInstallStrategy::BottleOrFail => return false,
                        NodeInstallStrategy::BottlePreferred => {}
                    }
                }
            }
            if let JobAction::Upgrade {
                old_install_path, ..
            } = action
            {
                if planner
                    .get_previous_installation_type(old_install_path)
                    .as_deref()
                    == Some("source")
                {
                    return true;
                }
            }
            !sps_core::install::bottle::has_bottle_for_current_platform(formula_arc)
        }
        InstallTargetIdentifier::Cask(_) => false,
    }
}

fn sort_planned_jobs(jobs: &mut [PlannedJob], formula_graph: &ResolvedGraph) {
    let formula_order: HashMap<String, usize> = formula_graph
        .install_plan
        .iter()
        .enumerate()
        .map(|(idx, dep_detail)| (dep_detail.formula.name().to_string(), idx))
        .collect();

    jobs.sort_by_key(|job| match &job.target_definition {
        InstallTargetIdentifier::Formula(f_arc) => formula_order
            .get(f_arc.name())
            .copied()
            .unwrap_or(usize::MAX),
        InstallTargetIdentifier::Cask(_) => usize::MAX - 1,
    });
}

// sps-core/src/check/installed.rs
use std::fs::{self}; // Removed DirEntry as it's not directly used here
use std::io;
use std::path::PathBuf;
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::keg::KegRegistry; // KegRegistry is used
use tracing::{debug, warn};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageType {
    Formula,
    Cask,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPackageInfo {
    pub name: String,
    pub version: String, // This will now store keg.version_str
    pub pkg_type: PackageType,
    pub path: PathBuf,
}

// Helper closure to handle io::Result<DirEntry> -> Option<DirEntry> logging errors
// Defined outside the functions to avoid repetition
fn handle_dir_entry(res: io::Result<fs::DirEntry>, dir_path_str: &str) -> Option<fs::DirEntry> {
    match res {
        Ok(entry) => Some(entry),
        Err(e) => {
            warn!("Error reading entry in {}: {}", dir_path_str, e);
            None
        }
    }
}

pub async fn get_installed_packages(config: &Config) -> Result<Vec<InstalledPackageInfo>> {
    let mut installed = Vec::new();
    let keg_registry = KegRegistry::new(config.clone());

    match keg_registry.list_installed_kegs() {
        Ok(kegs) => {
            for keg in kegs {
                installed.push(InstalledPackageInfo {
                    name: keg.name,
                    version: keg.version_str, // Use keg.version_str
                    pkg_type: PackageType::Formula,
                    path: keg.path,
                });
            }
        }
        Err(e) => warn!("Failed to list installed formulae: {}", e),
    }

    let caskroom_dir = config.cask_room_dir();
    if caskroom_dir.is_dir() {
        let caskroom_dir_str = caskroom_dir.to_str().unwrap_or("caskroom").to_string();
        let cask_token_entries_iter =
            fs::read_dir(&caskroom_dir).map_err(|e| SpsError::Io(Arc::new(e)))?;

        for entry_res in cask_token_entries_iter {
            if let Some(entry) = handle_dir_entry(entry_res, &caskroom_dir_str) {
                let cask_token_path = entry.path();
                if !cask_token_path.is_dir() {
                    continue;
                }
                let cask_token = entry.file_name().to_string_lossy().to_string();

                match fs::read_dir(&cask_token_path) {
                    Ok(version_entries_iter) => {
                        for version_entry_res in version_entries_iter {
                            if let Some(version_entry) = handle_dir_entry(
                                version_entry_res,
                                cask_token_path.to_str().unwrap_or("token_path"),
                            ) {
                                let version_path = version_entry.path();
                                if version_path.is_dir()
                                    && version_path.join("CASK_INSTALL_MANIFEST.json").is_file()
                                {
                                    let version_str =
                                        version_entry.file_name().to_string_lossy().to_string();
                                    let manifest_path =
                                        version_path.join("CASK_INSTALL_MANIFEST.json");
                                    let mut include = true;
                                    if manifest_path.is_file() {
                                        if let Ok(manifest_str) =
                                            std::fs::read_to_string(&manifest_path)
                                        {
                                            if let Ok(manifest_json) =
                                                serde_json::from_str::<serde_json::Value>(
                                                    &manifest_str,
                                                )
                                            {
                                                if let Some(is_installed) = manifest_json
                                                    .get("is_installed")
                                                    .and_then(|v| v.as_bool())
                                                {
                                                    include = is_installed;
                                                }
                                            }
                                        }
                                    }
                                    if include {
                                        installed.push(InstalledPackageInfo {
                                            name: cask_token.clone(),
                                            version: version_str,
                                            pkg_type: PackageType::Cask,
                                            path: version_path,
                                        });
                                    }
                                    // Assuming one actively installed version per cask token based
                                    // on manifest logic
                                    // If multiple version folders exist but only one manifest says
                                    // is_installed=true, this is fine.
                                    // If the intent is to list *all* version folders, the break
                                    // might be removed,
                                    // but then "is_installed" logic per version becomes more
                                    // important.
                                    // For now, finding the first "active" one is usually sufficient
                                    // for list/upgrade checks.
                                }
                            }
                        }
                    }
                    Err(e) => warn!("Failed to read cask versions for {}: {}", cask_token, e),
                }
            }
        }
    } else {
        debug!(
            "Caskroom directory {} does not exist.",
            caskroom_dir.display()
        );
    }
    Ok(installed)
}

pub async fn get_installed_package(
    name: &str,
    config: &Config,
) -> Result<Option<InstalledPackageInfo>> {
    let keg_registry = KegRegistry::new(config.clone());
    if let Some(keg) = keg_registry.get_installed_keg(name)? {
        return Ok(Some(InstalledPackageInfo {
            name: keg.name,
            version: keg.version_str, // Use keg.version_str
            pkg_type: PackageType::Formula,
            path: keg.path,
        }));
    }

    let cask_token_path = config.cask_room_token_path(name);
    if cask_token_path.is_dir() {
        let version_entries_iter =
            fs::read_dir(&cask_token_path).map_err(|e| SpsError::Io(Arc::new(e)))?;
        for version_entry_res in version_entries_iter {
            if let Some(version_entry) = handle_dir_entry(
                version_entry_res,
                cask_token_path.to_str().unwrap_or("token_path"),
            ) {
                let version_path = version_entry.path();
                if version_path.is_dir()
                    && version_path.join("CASK_INSTALL_MANIFEST.json").is_file()
                {
                    let version_str = version_entry.file_name().to_string_lossy().to_string();
                    let manifest_path = version_path.join("CASK_INSTALL_MANIFEST.json");
                    let mut include = true;
                    if manifest_path.is_file() {
                        if let Ok(manifest_str) = std::fs::read_to_string(&manifest_path) {
                            if let Ok(manifest_json) =
                                serde_json::from_str::<serde_json::Value>(&manifest_str)
                            {
                                if let Some(is_installed) =
                                    manifest_json.get("is_installed").and_then(|v| v.as_bool())
                                {
                                    include = is_installed;
                                }
                            }
                        }
                    }
                    if include {
                        return Ok(Some(InstalledPackageInfo {
                            name: name.to_string(),
                            version: version_str,
                            pkg_type: PackageType::Cask,
                            path: version_path,
                        }));
                    }
                }
            }
        }
    }
    Ok(None)
}

use std::path::Path;
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::cask::Cask;
use tracing::debug;

use crate::install::cask::InstalledArtifact;

/// Execute any `preflight` commands listed in the Cask’s JSON artifact stanza.
/// Returns an empty Vec since preflight does not produce install artifacts.
pub fn run_preflight(
    cask: &Cask,
    stage_path: &Path,
    _config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    // Iterate over artifacts, look for "preflight" keys
    if let Some(entries) = &cask.artifacts {
        for entry in entries.iter().filter_map(|v| v.as_object()) {
            if let Some(cmds) = entry.get("preflight").and_then(|v| v.as_array()) {
                for cmd_val in cmds.iter().filter_map(|v| v.as_str()) {
                    // Substitute $STAGEDIR placeholder
                    let cmd_str = cmd_val.replace("$STAGEDIR", stage_path.to_str().unwrap());
                    debug!("Running preflight: {}", cmd_str);
                    let status = Command::new("sh").arg("-c").arg(&cmd_str).status()?;
                    if !status.success() {
                        return Err(SpsError::InstallError(format!(
                            "preflight failed: {cmd_str}"
                        )));
                    }
                }
            }
        }
    }

    // No install artifacts to return
    Ok(Vec::new())
}

// ===== sps-common/src/model/cask.rs ===== // Corrected path
use std::collections::HashMap;
use std::fs;

use serde::{Deserialize, Serialize};

use crate::config::Config;

pub type Artifact = serde_json::Value;

/// Represents the `url` field, which can be a simple string or a map with specs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UrlField {
    Simple(String),
    WithSpec {
        url: String,
        #[serde(default)]
        verified: Option<String>,
        #[serde(flatten)]
        other: HashMap<String, serde_json::Value>,
    },
}

/// Represents the `sha256` field: hex, no_check, or per-architecture
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Sha256Field {
    Hex(String),
    #[serde(rename_all = "snake_case")]
    NoCheck {
        no_check: bool,
    },
    PerArch(HashMap<String, String>),
}

/// Appcast metadata
#[derive(Debug, Clone, Serialize, Deserialize)] // Ensure Serialize/Deserialize are here
pub struct Appcast {
    pub url: String,
    pub checkpoint: Option<String>,
}

/// Represents conflicts with other casks or formulae
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictsWith {
    #[serde(default)]
    pub cask: Vec<String>,
    #[serde(default)]
    pub formula: Vec<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

/// Represents the specific architecture details found in some cask definitions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchSpec {
    #[serde(rename = "type")] // Map the JSON "type" field
    pub type_name: String, // e.g., "arm"
    pub bits: u32, // e.g., 64
}

/// Helper for architecture requirements: single string, list of strings, or list of spec objects
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ArchReq {
    One(String),       // e.g., "arm64"
    Many(Vec<String>), // e.g., ["arm64", "x86_64"]
    Specs(Vec<ArchSpec>),
}

/// Helper for macOS requirements: symbol, list, comparison, or map
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MacOSReq {
    Symbol(String),       // ":big_sur"
    Symbols(Vec<String>), // [":catalina", ":big_sur"]
    Comparison(String),   // ">= :big_sur"
    Map(HashMap<String, Vec<String>>),
}

/// Helper to coerce string-or-list into Vec<String>
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringList {
    One(String),
    Many(Vec<String>),
}

impl From<StringList> for Vec<String> {
    fn from(item: StringList) -> Self {
        match item {
            StringList::One(s) => vec![s],
            StringList::Many(v) => v,
        }
    }
}

/// Represents `depends_on` block with multiple possible keys
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DependsOn {
    #[serde(default)]
    pub cask: Vec<String>,
    #[serde(default)]
    pub formula: Vec<String>,
    #[serde(default)]
    pub arch: Option<ArchReq>,
    #[serde(default)]
    pub macos: Option<MacOSReq>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

/// The main Cask model matching Homebrew JSON v2
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Cask {
    pub token: String,

    #[serde(default)]
    pub name: Option<Vec<String>>,
    pub version: Option<String>,
    pub desc: Option<String>,
    pub homepage: Option<String>,

    #[serde(default)]
    pub artifacts: Option<Vec<Artifact>>,

    #[serde(default)]
    pub url: Option<UrlField>,
    #[serde(default)]
    pub url_specs: Option<HashMap<String, serde_json::Value>>,

    #[serde(default)]
    pub sha256: Option<Sha256Field>,

    pub appcast: Option<Appcast>,
    pub auto_updates: Option<bool>,

    #[serde(default)]
    pub depends_on: Option<DependsOn>,

    #[serde(default)]
    pub conflicts_with: Option<ConflictsWith>,

    pub caveats: Option<String>,
    pub stage_only: Option<bool>,

    #[serde(default)]
    pub uninstall: Option<HashMap<String, serde_json::Value>>,

    #[serde(default)] // Only one default here
    pub zap: Option<Vec<ZapStanza>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaskList {
    pub casks: Vec<Cask>,
}

// --- ZAP STANZA SUPPORT ---

/// Helper for zap: string or array of strings
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrVec {
    String(String),
    Vec(Vec<String>),
}
impl StringOrVec {
    pub fn into_vec(self) -> Vec<String> {
        match self {
            StringOrVec::String(s) => vec![s],
            StringOrVec::Vec(v) => v,
        }
    }
}

/// Zap action details (trash, delete, rmdir, pkgutil, launchctl, script, signal, etc)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ZapActionDetail {
    Trash(Vec<String>),
    Delete(Vec<String>),
    Rmdir(Vec<String>),
    Pkgutil(StringOrVec),
    Launchctl(StringOrVec),
    Script {
        executable: String,
        args: Option<Vec<String>>,
    },
    Signal(Vec<String>),
    // Add more as needed
}

/// A zap stanza is a map of action -> detail
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZapStanza(pub std::collections::HashMap<String, ZapActionDetail>);

// --- Cask Impl ---

impl Cask {
    /// Check if this cask is installed by looking for a manifest file
    /// in any versioned directory within the Caskroom.
    pub fn is_installed(&self, config: &Config) -> bool {
        let cask_dir = config.cask_room_token_path(&self.token); // e.g., /opt/sps/cask_room/firefox
        if !cask_dir.exists() || !cask_dir.is_dir() {
            return false;
        }

        // Iterate through entries (version dirs) inside the cask_dir
        match fs::read_dir(&cask_dir) {
            Ok(entries) => {
                // Clippy fix: Use flatten() to handle Result entries directly
                for entry in entries.flatten() {
                    // <-- Use flatten() here
                    let version_path = entry.path();
                    // Check if it's a directory (representing a version)
                    if version_path.is_dir() {
                        // Check for the existence of the manifest file
                        let manifest_path = version_path.join("CASK_INSTALL_MANIFEST.json"); // <-- Correct filename
                        if manifest_path.is_file() {
                            // Check is_installed flag in manifest
                            let mut include = true;
                            if let Ok(manifest_str) = std::fs::read_to_string(&manifest_path) {
                                if let Ok(manifest_json) =
                                    serde_json::from_str::<serde_json::Value>(&manifest_str)
                                {
                                    if let Some(is_installed) =
                                        manifest_json.get("is_installed").and_then(|v| v.as_bool())
                                    {
                                        include = is_installed;
                                    }
                                }
                            }
                            if include {
                                // Found a manifest in at least one version directory, consider it
                                // installed
                                return true;
                            }
                        }
                    }
                }
                // If loop completes without finding a manifest in any version dir
                false
            }
            Err(e) => {
                // Log error if reading the directory fails, but assume not installed
                tracing::warn!(
                    "Failed to read cask directory {} to check for installed versions: {}",
                    cask_dir.display(),
                    e
                );
                false
            }
        }
    }

    /// Get the installed version of this cask by reading the directory names
    /// in the Caskroom. Returns the first version found (use cautiously if multiple
    /// versions could exist, though current install logic prevents this).
    pub fn installed_version(&self, config: &Config) -> Option<String> {
        let cask_dir = config.cask_room_token_path(&self.token); //
        if !cask_dir.exists() {
            return None;
        }
        // Iterate through entries and return the first directory name found
        match fs::read_dir(&cask_dir) {
            Ok(entries) => {
                // Clippy fix: Use flatten()
                for entry in entries.flatten() {
                    // <-- Use flatten() here
                    let path = entry.path();
                    // Check if it's a directory (representing a version)
                    if path.is_dir() {
                        if let Some(version_str) = path.file_name().and_then(|name| name.to_str()) {
                            // Return the first version directory name found
                            return Some(version_str.to_string());
                        }
                    }
                }
                // No version directories found
                None
            }
            Err(_) => None, // Error reading directory
        }
    }

    /// Get a friendly name for display purposes
    pub fn display_name(&self) -> String {
        self.name
            .as_ref()
            .and_then(|names| names.first().cloned())
            .unwrap_or_else(|| self.token.clone())
    }
}

// sps-core/src/check/update.rs

use std::collections::HashMap;
use std::sync::Arc;

// Imports from sps-common
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::formulary::Formulary; // Using the shared Formulary
use sps_common::model::version::Version as PkgVersion;
use sps_common::model::Cask; // Using the Cask and Formula from sps-common
// Use the Cask and Formula structs from sps_common::model
// Ensure InstallTargetIdentifier is correctly pathed if it's also in sps_common::model
use sps_common::model::InstallTargetIdentifier;
// Imports from sps-net
use sps_net::api;

// Imports from sps-core
use crate::check::installed::{InstalledPackageInfo, PackageType};

#[derive(Debug, Clone)]
pub struct UpdateInfo {
    pub name: String,
    pub installed_version: String,
    pub available_version: String,
    pub pkg_type: PackageType,
    pub target_definition: InstallTargetIdentifier,
}

/// Ensures that the raw JSON data for formulas and casks exists in the main disk cache,
/// fetching from the API if necessary.
async fn ensure_api_data_cached(cache: &Cache) -> Result<()> {
    let formula_check = cache.load_raw("formula.json");
    let cask_check = cache.load_raw("cask.json");

    // Determine if fetches are needed
    let mut fetch_formulas = false;
    if formula_check.is_err() {
        tracing::debug!("Local formula.json cache missing or unreadable. Scheduling fetch.");
        fetch_formulas = true;
    }

    let mut fetch_casks = false;
    if cask_check.is_err() {
        tracing::debug!("Local cask.json cache missing or unreadable. Scheduling fetch.");
        fetch_casks = true;
    }

    // Perform fetches concurrently if needed
    match (fetch_formulas, fetch_casks) {
        (true, true) => {
            tracing::debug!("Populating missing formula.json and cask.json from API...");
            let (formula_res, cask_res) = tokio::join!(
                async {
                    let data = api::fetch_all_formulas().await?;
                    cache.store_raw("formula.json", &data)?;
                    Ok::<(), SpsError>(())
                },
                async {
                    let data = api::fetch_all_casks().await?;
                    cache.store_raw("cask.json", &data)?;
                    Ok::<(), SpsError>(())
                }
            );
            formula_res?;
            cask_res?;
            tracing::debug!("Formula.json and cask.json populated from API.");
        }
        (true, false) => {
            tracing::debug!("Populating missing formula.json from API...");
            let data = api::fetch_all_formulas().await?;
            cache.store_raw("formula.json", &data)?;
            tracing::debug!("Formula.json populated from API.");
        }
        (false, true) => {
            tracing::debug!("Populating missing cask.json from API...");
            let data = api::fetch_all_casks().await?;
            cache.store_raw("cask.json", &data)?;
            tracing::debug!("Cask.json populated from API.");
        }
        (false, false) => {
            tracing::debug!("Local formula.json and cask.json caches are present.");
        }
    }
    Ok(())
}

pub async fn check_for_updates(
    installed_packages: &[InstalledPackageInfo],
    cache: &Cache,
    config: &Config,
) -> Result<Vec<UpdateInfo>> {
    // 1. Ensure the underlying JSON files in the main cache are populated.
    ensure_api_data_cached(cache)
        .await
        .map_err(|e| {
            tracing::error!(
                "Failed to ensure API data is cached for update check: {}",
                e
            );
            // Decide if this is a fatal error for update checking or if we can proceed with
            // potentially stale/missing data. For now, let's make it non-fatal but log
            // an error, as Formulary/cask parsing might still work with older cache.
            SpsError::Generic(format!(
                "Failed to ensure API data in cache, update check might be unreliable: {e}"
            ))
        })
        .ok(); // Continue even if this pre-fetch step fails, rely on Formulary/cask loading to handle actual
               // errors.

    // 2. Instantiate Formulary. It uses the `cache` (from sps-common) to load `formula.json`.
    let formulary = Formulary::new(config.clone());

    // 3. For Casks: Load the entire `cask.json` from cache and parse it robustly into Vec<Cask>.
    //    This uses the Cask definition from sps_common::model::cask.
    let casks_map: HashMap<String, Arc<Cask>> = match cache.load_raw("cask.json") {
        Ok(json_str) => {
            // Parse directly into Vec<Cask> using the definition from sps-common::model::cask
            match serde_json::from_str::<Vec<Cask>>(&json_str) {
                Ok(casks) => casks
                    .into_iter()
                    .map(|c| (c.token.clone(), Arc::new(c)))
                    .collect(),
                Err(e) => {
                    tracing::warn!(
                        "Failed to parse full cask.json string into Vec<Cask> (from sps-common): {}. Cask update checks may be incomplete.",
                        e
                    );
                    HashMap::new()
                }
            }
        }
        Err(e) => {
            tracing::warn!(
                "Failed to load cask.json string from cache: {}. Cask update checks will be based on no remote data.", e
            );
            HashMap::new()
        }
    };

    tracing::debug!(
        "[UpdateCheck] Starting update check for {} packages",
        installed_packages.len()
    );
    let mut updates_available = Vec::new();

    for installed in installed_packages {
        tracing::debug!(
            "[UpdateCheck] Checking package '{}' version '{}'",
            installed.name,
            installed.version
        );
        match installed.pkg_type {
            PackageType::Formula => {
                match formulary.load_formula(&installed.name) {
                    // Uses sps-common::formulary::Formulary
                    Ok(latest_formula_obj) => {
                        // Returns sps_common::model::formula::Formula
                        let latest_formula_arc = Arc::new(latest_formula_obj);

                        let latest_version_str = latest_formula_arc.version_str_full();
                        tracing::debug!(
                            "[UpdateCheck] Formula '{}': installed='{}', latest='{}'",
                            installed.name,
                            installed.version,
                            latest_version_str
                        );

                        let installed_v_res = PkgVersion::parse(&installed.version);
                        let latest_v_res = PkgVersion::parse(&latest_version_str);
                        let installed_revision = installed
                            .version
                            .split('_')
                            .nth(1)
                            .and_then(|s| s.parse::<u32>().ok())
                            .unwrap_or(0);

                        let needs_update = match (installed_v_res, latest_v_res) {
                            (Ok(iv), Ok(lv)) => {
                                let version_newer = lv > iv;
                                let revision_newer =
                                    lv == iv && latest_formula_arc.revision > installed_revision;
                                tracing::debug!("[UpdateCheck] Formula '{}': version_newer={}, revision_newer={} (installed_rev={}, latest_rev={})",
                                    installed.name, version_newer, revision_newer, installed_revision, latest_formula_arc.revision);
                                version_newer || revision_newer
                            }
                            _ => {
                                let different = installed.version != latest_version_str;
                                tracing::debug!("[UpdateCheck] Formula '{}': fallback string comparison, different={}",
                                    installed.name, different);
                                different
                            }
                        };

                        tracing::debug!(
                            "[UpdateCheck] Formula '{}': needs_update={}",
                            installed.name,
                            needs_update
                        );
                        if needs_update {
                            updates_available.push(UpdateInfo {
                                name: installed.name.clone(),
                                installed_version: installed.version.clone(),
                                available_version: latest_version_str,
                                pkg_type: PackageType::Formula,
                                target_definition: InstallTargetIdentifier::Formula(
                                    // From sps-common
                                    latest_formula_arc.clone(),
                                ),
                            });
                        }
                    }
                    Err(_e) => {
                        tracing::debug!(
                                "Installed formula '{}' not found via Formulary. It might have been removed from the remote. Source error: {}",
                                installed.name, _e
                            );
                    }
                }
            }
            PackageType::Cask => {
                if let Some(latest_cask_arc) = casks_map.get(&installed.name) {
                    // latest_cask_arc is Arc<sps_common::model::cask::Cask>
                    if let Some(available_version) = latest_cask_arc.version.as_ref() {
                        if &installed.version != available_version {
                            updates_available.push(UpdateInfo {
                                name: installed.name.clone(),
                                installed_version: installed.version.clone(),
                                available_version: available_version.clone(),
                                pkg_type: PackageType::Cask,
                                target_definition: InstallTargetIdentifier::Cask(
                                    // From sps-common
                                    latest_cask_arc.clone(),
                                ),
                            });
                        }
                    } else {
                        tracing::warn!(
                            "Latest cask definition for '{}' from casks_map has no version string.",
                            installed.name
                        );
                    }
                } else {
                    tracing::warn!(
                        "Installed cask '{}' not found in the fully parsed casks_map.",
                        installed.name
                    );
                }
            }
        }
    }

    tracing::debug!(
        "[UpdateCheck] Update check complete: {} updates available out of {} packages checked",
        updates_available.len(),
        installed_packages.len()
    );
    tracing::debug!(
        "[UpdateCheck] Updates available for: {:?}",
        updates_available
            .iter()
            .map(|u| &u.name)
            .collect::<Vec<_>>()
    );

    Ok(updates_available)
}

use std::path::PathBuf;

use sps_common::error::Result;
use sps_common::model::artifact::InstalledArtifact;
use sps_common::model::cask::Cask;

/// At install time, scan the `uninstall` stanza and turn each directive
/// into an InstalledArtifact variant, so it can later be torn down.
pub fn record_uninstall(cask: &Cask) -> Result<Vec<InstalledArtifact>> {
    let mut artifacts = Vec::new();

    if let Some(entries) = &cask.artifacts {
        for entry in entries.iter().filter_map(|v| v.as_object()) {
            if let Some(steps) = entry.get("uninstall").and_then(|v| v.as_array()) {
                for step in steps.iter().filter_map(|v| v.as_object()) {
                    for (key, val) in step {
                        match key.as_str() {
                            "pkgutil" => {
                                if let Some(id) = val.as_str() {
                                    artifacts.push(InstalledArtifact::PkgUtilReceipt {
                                        id: id.to_string(),
                                    });
                                }
                            }
                            "delete" => {
                                if let Some(arr) = val.as_array() {
                                    for p in arr.iter().filter_map(|v| v.as_str()) {
                                        artifacts.push(InstalledArtifact::MovedResource {
                                            path: PathBuf::from(p),
                                        });
                                    }
                                }
                            }
                            "rmdir" => {
                                if let Some(arr) = val.as_array() {
                                    for p in arr.iter().filter_map(|v| v.as_str()) {
                                        artifacts.push(InstalledArtifact::MovedResource {
                                            path: PathBuf::from(p),
                                        });
                                    }
                                }
                            }
                            "launchctl" => {
                                if let Some(arr) = val.as_array() {
                                    for lbl in arr.iter().filter_map(|v| v.as_str()) {
                                        artifacts.push(InstalledArtifact::Launchd {
                                            label: lbl.to_string(),
                                            path: None,
                                        });
                                    }
                                }
                            }
                            // Add other uninstall keys similarly...
                            _ => {}
                        }
                    }
                }
            }
        }
    }

    Ok(artifacts)
}

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use clap::Args;
use colored::Colorize;
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::cask::Cask;
use sps_common::Cache;
use sps_core::check::{installed, PackageType};
use sps_core::{uninstall as core_uninstall, UninstallOptions};
use sps_net::api;
use tracing::{debug, error, warn};
use {serde_json, walkdir};

#[derive(Args, Debug)]
pub struct Uninstall {
    /// The names of the formulas or casks to uninstall
    #[arg(required = true)] // Ensure at least one name is given
    pub names: Vec<String>,
    /// Perform a deep clean for casks, removing associated user data, caches,
    /// and configuration files. Use with caution, data will be lost! Ignored for formulas.
    #[arg(
        long,
        help = "Perform a deep clean for casks, removing associated user data, caches, and configuration files. Use with caution!"
    )]
    pub zap: bool,
}

impl Uninstall {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        let names = &self.names;
        let mut errors: Vec<(String, SpsError)> = Vec::new();

        for name in names {
            // Basic name validation to prevent path traversal
            if name.contains('/') || name.contains("..") {
                let msg = format!("Invalid package name '{name}' contains disallowed characters");
                error!("✖ {msg}");
                errors.push((name.to_string(), SpsError::Generic(msg)));
                continue;
            }

            println!("Uninstalling {name}...");

            match installed::get_installed_package(name, config).await {
                Ok(Some(installed_info)) => {
                    let (file_count, size_bytes) =
                        count_files_and_size(&installed_info.path).unwrap_or((0, 0));
                    let uninstall_opts = UninstallOptions { skip_zap: false };
                    debug!(
                        "Attempting uninstall for {} ({:?})",
                        name, installed_info.pkg_type
                    );
                    let uninstall_result = match installed_info.pkg_type {
                        PackageType::Formula => {
                            if self.zap {
                                warn!("--zap flag is ignored for formulas like '{}'.", name);
                            }
                            core_uninstall::uninstall_formula_artifacts(
                                &installed_info,
                                config,
                                &uninstall_opts,
                            )
                        }
                        PackageType::Cask => {
                            core_uninstall::uninstall_cask_artifacts(&installed_info, config)
                        }
                    };

                    if let Err(e) = uninstall_result {
                        error!("✖ Failed to uninstall '{}': {}", name.cyan(), e);
                        errors.push((name.to_string(), e));
                        // Continue to zap anyway for casks, as per plan
                    } else {
                        println!(
                            "✓ Uninstalled {:?} {} ({} files, {})",
                            installed_info.pkg_type,
                            name.green(),
                            file_count,
                            format_size(size_bytes)
                        );
                    }

                    // --- Zap Uninstall (Conditional) ---
                    if self.zap && installed_info.pkg_type == PackageType::Cask {
                        println!("Zapping {name}...");
                        debug!(
                            "--zap specified for cask '{}', attempting deep clean.",
                            name
                        );

                        // Fetch the Cask definition (needed for the zap stanza)
                        let cask_def_result: Result<Cask> = async {
                            match api::get_cask(name).await {
                                Ok(cask) => Ok(cask),
                                Err(e) => {
                                    warn!("Failed API fetch for zap definition for '{}' ({}), trying cache...", name, e);
                                    match cache.load_raw("cask.json") {
                                        Ok(raw_json) => {
                                            let casks: Vec<Cask> = serde_json::from_str(&raw_json)
                                                .map_err(|cache_e| SpsError::Cache(format!("Failed parse cached cask.json: {cache_e}")))?;
                                            casks.into_iter()
                                                .find(|c| c.token == *name)
                                                .ok_or_else(|| SpsError::NotFound(format!("Cask '{name}' def not in cache either")))
                                        }
                                        Err(cache_e) => Err(SpsError::Cache(format!("Failed load cask cache for zap: {cache_e}"))),
                                    }
                                }
                            }
                        }.await;

                        match cask_def_result {
                            Ok(cask_def) => {
                                match core_uninstall::zap_cask_artifacts(
                                    &installed_info,
                                    &cask_def,
                                    config,
                                )
                                .await
                                {
                                    Ok(_) => {
                                        println!("✓ Zap complete for {}", name.green());
                                    }
                                    Err(zap_err) => {
                                        error!("✖ Zap failed for '{}': {}", name.cyan(), zap_err);
                                        errors.push((format!("{name} (zap)"), zap_err));
                                    }
                                }
                            }
                            Err(e) => {
                                error!(
                                    "✖ Could not get Cask definition for zap '{}': {}",
                                    name.cyan(),
                                    e
                                );
                                errors.push((format!("{name} (zap definition)"), e));
                            }
                        }
                    }
                }
                Ok(None) => {
                    let msg = format!("Package '{name}' is not installed.");
                    error!("✖ {msg}");
                    errors.push((name.to_string(), SpsError::NotFound(msg)));
                }
                Err(e) => {
                    let msg = format!("Failed check install status for '{name}': {e}");
                    error!("✖ {msg}");
                    errors.push((name.clone(), SpsError::Generic(msg)));
                }
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            eprintln!("\n{}:", "Finished uninstalling with errors".yellow());
            let mut errors_by_pkg: HashMap<String, Vec<String>> = HashMap::new();
            for (pkg_name, error) in errors {
                errors_by_pkg
                    .entry(pkg_name)
                    .or_default()
                    .push(error.to_string());
            }
            for (pkg_name, error_list) in errors_by_pkg {
                eprintln!("Package '{}':", pkg_name.cyan());
                let unique_errors: HashSet<_> = error_list.into_iter().collect();
                for error_str in unique_errors {
                    eprintln!("- {}", error_str.red());
                }
            }
            Err(SpsError::Generic(
                "Uninstall failed for one or more packages.".to_string(),
            ))
        }
    }
}

// --- Unchanged Helper Functions ---
fn count_files_and_size(path: &std::path::Path) -> Result<(usize, u64)> {
    let mut file_count = 0;
    let mut total_size = 0;
    for entry in walkdir::WalkDir::new(path) {
        match entry {
            Ok(entry_data) => {
                if entry_data.file_type().is_file() || entry_data.file_type().is_symlink() {
                    match entry_data.metadata() {
                        Ok(metadata) => {
                            file_count += 1;
                            if entry_data.file_type().is_file() {
                                total_size += metadata.len();
                            }
                        }
                        Err(e) => {
                            tracing::warn!(
                                "Could not get metadata for {}: {}",
                                entry_data.path().display(),
                                e
                            );
                        }
                    }
                }
            }
            Err(e) => {
                tracing::warn!("Error traversing directory {}: {}", path.display(), e);
            }
        }
    }
    Ok((file_count, total_size))
}

fn format_size(size: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    if size >= GB {
        format!("{:.1}GB", size as f64 / GB as f64)
    } else if size >= MB {
        format!("{:.1}MB", size as f64 / MB as f64)
    } else if size >= KB {
        format!("{:.1}KB", size as f64 / KB as f64)
    } else {
        format!("{size}B")
    }
}

// sps-core/src/uninstall/common.rs

use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::{fs, io};

use sps_common::config::Config;
use tracing::{debug, error, warn};

#[derive(Debug, Clone, Default)]
pub struct UninstallOptions {
    pub skip_zap: bool,
}

/// Removes a filesystem artifact (file or directory).
///
/// Attempts direct removal. If `use_sudo` is true and direct removal
/// fails due to permission errors, it will attempt `sudo rm -rf`.
///
/// Returns `true` if the artifact is successfully removed or was already gone,
/// `false` otherwise.
pub(crate) fn remove_filesystem_artifact(path: &Path, use_sudo: bool) -> bool {
    match path.symlink_metadata() {
        Ok(metadata) => {
            let file_type = metadata.file_type();
            // A directory is only a "real" directory if it's not a symlink.
            // Symlinks to directories should be removed with remove_file.
            let is_real_dir = file_type.is_dir();

            debug!(
                "Removing filesystem artifact ({}) at: {}",
                if is_real_dir {
                    "directory"
                } else if file_type.is_symlink() {
                    "symlink"
                } else {
                    "file"
                },
                path.display()
            );

            let remove_op = || -> io::Result<()> {
                if is_real_dir {
                    fs::remove_dir_all(path)
                } else {
                    // This handles both files and symlinks
                    fs::remove_file(path)
                }
            };

            if let Err(e) = remove_op() {
                if use_sudo && e.kind() == io::ErrorKind::PermissionDenied {
                    warn!(
                        "Direct removal failed (Permission Denied). Trying with sudo rm -rf: {}",
                        path.display()
                    );
                    let output = Command::new("sudo").arg("rm").arg("-rf").arg(path).output();
                    match output {
                        Ok(out) if out.status.success() => {
                            debug!("Successfully removed {} with sudo.", path.display());
                            true
                        }
                        Ok(out) => {
                            error!(
                                "Failed to remove {} with sudo: {}",
                                path.display(),
                                String::from_utf8_lossy(&out.stderr).trim()
                            );
                            false
                        }
                        Err(sudo_err) => {
                            error!(
                                "Error executing sudo rm for {}: {}",
                                path.display(),
                                sudo_err
                            );
                            false
                        }
                    }
                } else if e.kind() != io::ErrorKind::NotFound {
                    error!("Failed to remove artifact {}: {}", path.display(), e);
                    false
                } else {
                    debug!("Artifact {} already removed.", path.display());
                    true
                }
            } else {
                debug!("Successfully removed artifact: {}", path.display());
                true
            }
        }
        Err(e) if e.kind() == io::ErrorKind::NotFound => {
            debug!("Artifact not found (already removed?): {}", path.display());
            true
        }
        Err(e) => {
            warn!(
                "Failed to get metadata for artifact {}: {}",
                path.display(),
                e
            );
            false
        }
    }
}

/// Expands a path string that may start with `~` to the user's home directory.
pub(crate) fn expand_tilde(path_str: &str, home: &Path) -> PathBuf {
    if let Some(stripped) = path_str.strip_prefix("~/") {
        home.join(stripped)
    } else {
        PathBuf::from(path_str)
    }
}

/// Checks if a path is safe for zap operations.
/// Safe paths are typically within user Library, .config, /Applications, /Library,
/// or the sps cache directory. Root, home, /Applications, /Library themselves are not safe.
pub(crate) fn is_safe_path(path: &Path, home: &Path, config: &Config) -> bool {
    if path.components().any(|c| matches!(c, Component::ParentDir)) {
        warn!("Zap path rejected (contains '..'): {}", path.display());
        return false;
    }
    let allowed_roots = [
        home.join("Library"),
        home.join(".config"),
        PathBuf::from("/Applications"),
        PathBuf::from("/Library"),
        config.cache_dir().clone(),
        // Consider adding more specific allowed user dirs if necessary
    ];

    // Check if the path is exactly one of the top-level restricted paths
    if path == Path::new("/")
        || path == home
        || path == Path::new("/Applications")
        || path == Path::new("/Library")
    {
        warn!("Zap path rejected (too broad): {}", path.display());
        return false;
    }

    if allowed_roots.iter().any(|root| path.starts_with(root)) {
        return true;
    }

    warn!(
        "Zap path rejected (outside allowed areas): {}",
        path.display()
    );
    false
}

// sps-core/src/build/formula/macho.rs
// Contains Mach-O specific patching logic for bottle relocation.
// Updated to use MachOFatFile32 and MachOFatFile64 for FAT binary parsing.
// Refactored to separate immutable analysis from mutable patching to fix borrow checker errors.

use std::collections::HashMap;
use std::fs;
use std::io::Write; // Keep for write_patched_buffer
use std::path::Path;
use std::process::{Command as StdCommand, Stdio}; // Keep for codesign

// --- Imports needed for Mach-O patching (macOS only) ---
#[cfg(target_os = "macos")]
use object::{
    self,
    macho::{MachHeader32, MachHeader64}, // Keep for Mach-O parsing
    read::macho::{
        FatArch,
        LoadCommandVariant, // Correct import path
        MachHeader,
        MachOFatFile32,
        MachOFatFile64, // Core Mach-O types + FAT types
        MachOFile,
    },
    Endianness,
    FileKind,
    ReadRef,
};
use sps_common::error::{Result, SpsError};
use tempfile::NamedTempFile;
use tracing::{debug, error};

// --- Platform‑specific constants for Mach‑O magic detection ---
#[cfg(target_os = "macos")]
const MH_MAGIC: u32 = 0xfeedface;
#[cfg(target_os = "macos")]
const MH_MAGIC_64: u32 = 0xfeedfacf;
#[cfg(target_os = "macos")]
const MACHO_HEADER32_SIZE: usize = 28;
#[cfg(target_os = "macos")]
const MACHO_HEADER64_SIZE: usize = 32;

/// Core patch data for **one** string replacement location inside a Mach‑O file.
#[cfg(target_os = "macos")]
#[derive(Debug, Clone)]
struct PatchInfo {
    absolute_offset: usize, // Offset in the entire file buffer
    allocated_len: usize,   // How much space was allocated for this string
    new_path: String,       // The new string to write
}

/// Container for paths that couldn't be patched due to length constraints
#[cfg(target_os = "macos")]
#[derive(Debug, Clone)]
pub struct SkippedPath {
    pub old_path: String,
    pub new_path: String,
}

/// Main entry point for Mach‑O path patching (macOS only).
/// Returns `Ok(true)` if patches were applied, `Ok(false)` if no patches needed.
/// Returns a tuple: (patched: bool, skipped_paths: Vec<SkippedPath>)
#[cfg(target_os = "macos")]
pub fn patch_macho_file(
    path: &Path,
    replacements: &HashMap<String, String>,
) -> Result<(bool, Vec<SkippedPath>)> {
    patch_macho_file_macos(path, replacements)
}

/// Container for paths that couldn't be patched due to length constraints
#[cfg(not(target_os = "macos"))]
#[derive(Debug, Clone)]
pub struct SkippedPath {
    pub old_path: String,
    pub new_path: String,
}

/// No‑op stub for non‑macOS platforms.
#[cfg(not(target_os = "macos"))]
pub fn patch_macho_file(
    _path: &Path,
    _replacements: &HashMap<String, String>,
) -> Result<(bool, Vec<SkippedPath>)> {
    Ok((false, Vec::new()))
}

/// **macOS implementation**: Tries to patch Mach‑O files by replacing placeholders.
#[cfg(target_os = "macos")]
fn patch_macho_file_macos(
    path: &Path,
    replacements: &HashMap<String, String>,
) -> Result<(bool, Vec<SkippedPath>)> {
    debug!("Processing potential Mach-O file: {}", path.display());

    // 1) Load the entire file into memory
    let buffer = match fs::read(path) {
        Ok(data) => data,
        Err(e) => {
            debug!("Failed to read {}: {}", path.display(), e);
            return Ok((false, Vec::new()));
        }
    };
    if buffer.is_empty() {
        debug!("Empty file: {}", path.display());
        return Ok((false, Vec::new()));
    }

    // 2) Identify the file type
    let kind = match object::FileKind::parse(&*buffer) {
        Ok(k) => k,
        Err(_e) => {
            debug!("Not an object file: {}", path.display());
            return Ok((false, Vec::new()));
        }
    };

    // 3) **Analysis phase**: collect patches + skipped paths
    let (patches, skipped_paths) = collect_macho_patches(&buffer, kind, replacements, path)?;

    if patches.is_empty() {
        if skipped_paths.is_empty() {
            debug!("No patches needed for {}", path.display());
        } else {
            debug!(
                "No patches applied for {} ({} paths skipped due to length)",
                path.display(),
                skipped_paths.len()
            );
        }
        return Ok((false, skipped_paths));
    }

    // 4) Clone buffer and apply all patches atomically
    let mut patched_buffer = buffer;
    for patch in &patches {
        patch_path_in_buffer(
            &mut patched_buffer,
            patch.absolute_offset,
            patch.allocated_len,
            &patch.new_path,
            path,
        )?;
    }

    // 5) Write atomically
    write_patched_buffer(path, &patched_buffer)?;
    debug!("Wrote patched Mach-O: {}", path.display());

    // 6) Re‑sign on Apple Silicon
    #[cfg(target_arch = "aarch64")]
    {
        resign_binary(path)?;
        debug!("Re‑signed patched binary: {}", path.display());
    }

    Ok((true, skipped_paths))
}

/// ASCII magic for the start of a static `ar` archive  (`!<arch>\n`)
#[cfg(target_os = "macos")]
const AR_MAGIC: &[u8; 8] = b"!<arch>\n";

/// Examine a buffer (Mach‑O or FAT) and return every patch we must apply + skipped paths.
#[cfg(target_os = "macos")]
fn collect_macho_patches(
    buffer: &[u8],
    kind: FileKind,
    replacements: &HashMap<String, String>,
    path_for_log: &Path,
) -> Result<(Vec<PatchInfo>, Vec<SkippedPath>)> {
    let mut patches = Vec::<PatchInfo>::new();
    let mut skipped_paths = Vec::<SkippedPath>::new();

    match kind {
        /* ---------------------------------------------------------- */
        FileKind::MachO32 => {
            let m = MachOFile::<MachHeader32<Endianness>, _>::parse(buffer)?;
            let (mut p, mut s) =
                find_patches_in_commands(&m, 0, MACHO_HEADER32_SIZE, replacements, path_for_log)?;
            patches.append(&mut p);
            skipped_paths.append(&mut s);
        }
        /* ---------------------------------------------------------- */
        FileKind::MachO64 => {
            let m = MachOFile::<MachHeader64<Endianness>, _>::parse(buffer)?;
            let (mut p, mut s) =
                find_patches_in_commands(&m, 0, MACHO_HEADER64_SIZE, replacements, path_for_log)?;
            patches.append(&mut p);
            skipped_paths.append(&mut s);
        }
        /* ---------------------------------------------------------- */
        FileKind::MachOFat32 => {
            let fat = MachOFatFile32::parse(buffer)?;
            for (idx, arch) in fat.arches().iter().enumerate() {
                let (off, sz) = arch.file_range();
                let slice = &buffer[off as usize..(off + sz) as usize];

                /* short‑circuit: static .a archive inside FAT ---------- */
                if slice.starts_with(AR_MAGIC) {
                    debug!("[slice {}] static archive – skipped", idx);
                    continue;
                }

                /* decide 32 / 64 by magic ------------------------------ */
                if slice.len() >= 4 {
                    let magic = u32::from_le_bytes(slice[0..4].try_into().unwrap());
                    if magic == MH_MAGIC_64 {
                        if let Ok(m) = MachOFile::<MachHeader64<Endianness>, _>::parse(slice) {
                            let (mut p, mut s) = find_patches_in_commands(
                                &m,
                                off as usize,
                                MACHO_HEADER64_SIZE,
                                replacements,
                                path_for_log,
                            )?;
                            patches.append(&mut p);
                            skipped_paths.append(&mut s);
                        }
                    } else if magic == MH_MAGIC {
                        if let Ok(m) = MachOFile::<MachHeader32<Endianness>, _>::parse(slice) {
                            let (mut p, mut s) = find_patches_in_commands(
                                &m,
                                off as usize,
                                MACHO_HEADER32_SIZE,
                                replacements,
                                path_for_log,
                            )?;
                            patches.append(&mut p);
                            skipped_paths.append(&mut s);
                        }
                    }
                }
            }
        }
        /* ---------------------------------------------------------- */
        FileKind::MachOFat64 => {
            let fat = MachOFatFile64::parse(buffer)?;
            for (idx, arch) in fat.arches().iter().enumerate() {
                let (off, sz) = arch.file_range();
                let slice = &buffer[off as usize..(off + sz) as usize];

                if slice.starts_with(AR_MAGIC) {
                    debug!("[slice {}] static archive – skipped", idx);
                    continue;
                }

                if slice.len() >= 4 {
                    let magic = u32::from_le_bytes(slice[0..4].try_into().unwrap());
                    if magic == MH_MAGIC_64 {
                        if let Ok(m) = MachOFile::<MachHeader64<Endianness>, _>::parse(slice) {
                            let (mut p, mut s) = find_patches_in_commands(
                                &m,
                                off as usize,
                                MACHO_HEADER64_SIZE,
                                replacements,
                                path_for_log,
                            )?;
                            patches.append(&mut p);
                            skipped_paths.append(&mut s);
                        }
                    } else if magic == MH_MAGIC {
                        if let Ok(m) = MachOFile::<MachHeader32<Endianness>, _>::parse(slice) {
                            let (mut p, mut s) = find_patches_in_commands(
                                &m,
                                off as usize,
                                MACHO_HEADER32_SIZE,
                                replacements,
                                path_for_log,
                            )?;
                            patches.append(&mut p);
                            skipped_paths.append(&mut s);
                        }
                    }
                }
            }
        }
        /* ---------------------------------------------------------- */
        _ => { /* archives & unknown kinds are ignored */ }
    }

    Ok((patches, skipped_paths))
}

/// Iterates through load commands of a parsed MachOFile (slice) and returns
/// patch details + skipped paths.
#[cfg(target_os = "macos")]
fn find_patches_in_commands<'data, Mach, R>(
    macho_file: &MachOFile<'data, Mach, R>,
    slice_base_offset: usize,
    header_size: usize,
    replacements: &HashMap<String, String>,
    file_path_for_log: &Path,
) -> Result<(Vec<PatchInfo>, Vec<SkippedPath>)>
where
    Mach: MachHeader,
    R: ReadRef<'data>,
{
    let endian = macho_file.endian();
    let mut patches = Vec::new();
    let mut skipped_paths = Vec::new();
    let mut cur_off = header_size;

    let mut it = macho_file.macho_load_commands()?;
    while let Some(cmd) = it.next()? {
        let cmd_size = cmd.cmdsize() as usize;
        let cmd_offset = cur_off; // offset *inside this slice*
        cur_off += cmd_size;

        let variant = match cmd.variant() {
            Ok(v) => v,
            Err(e) => {
                tracing::warn!(
                    "Malformed load‑command in {}: {}; skipping",
                    file_path_for_log.display(),
                    e
                );
                continue;
            }
        };

        // — which commands carry path strings we might want? —
        let path_info: Option<(u32, &[u8])> = match variant {
            LoadCommandVariant::Dylib(d) | LoadCommandVariant::IdDylib(d) => cmd
                .string(endian, d.dylib.name)
                .ok()
                .map(|bytes| (d.dylib.name.offset.get(endian), bytes)),
            LoadCommandVariant::Rpath(r) => cmd
                .string(endian, r.path)
                .ok()
                .map(|bytes| (r.path.offset.get(endian), bytes)),
            _ => None,
        };

        if let Some((offset_in_cmd, bytes)) = path_info {
            if let Ok(old_path) = std::str::from_utf8(bytes) {
                if let Some(new_path) = find_and_replace_placeholders(old_path, replacements) {
                    let allocated = cmd_size.saturating_sub(offset_in_cmd as usize);

                    if new_path.len() + 1 > allocated {
                        // would overflow – add to skipped paths instead of throwing
                        tracing::debug!(
                            "Skip patch (too long): '{}' → '{}' (alloc {} B) in {}",
                            old_path,
                            new_path,
                            allocated,
                            file_path_for_log.display()
                        );
                        skipped_paths.push(SkippedPath {
                            old_path: old_path.to_string(),
                            new_path: new_path.clone(),
                        });
                        continue;
                    }

                    patches.push(PatchInfo {
                        absolute_offset: slice_base_offset + cmd_offset + offset_in_cmd as usize,
                        allocated_len: allocated,
                        new_path,
                    });
                }
            }
        }
    }
    Ok((patches, skipped_paths))
}

/// Helper to replace placeholders in a string based on the replacements map.
/// Returns `Some(String)` with replacements if any were made, `None` otherwise.
fn find_and_replace_placeholders(
    current_path: &str,
    replacements: &HashMap<String, String>,
) -> Option<String> {
    let mut new_path = current_path.to_string();
    let mut path_modified = false;
    // Iterate through all placeholder/replacement pairs
    for (placeholder, replacement) in replacements {
        // Check if the current path string contains the placeholder
        if new_path.contains(placeholder) {
            // Replace all occurrences of the placeholder
            new_path = new_path.replace(placeholder, replacement);
            path_modified = true; // Mark that a change was made
            debug!(
                "   Replaced '{}' with '{}' -> '{}'",
                placeholder, replacement, new_path
            );
        }
    }
    // Return the modified string only if changes occurred
    if path_modified {
        Some(new_path)
    } else {
        None
    }
}

/// Write a new (null‑padded) path into the mutable buffer.
/// Assumes the caller already verified the length.
#[cfg(target_os = "macos")]
fn patch_path_in_buffer(
    buf: &mut [u8],
    abs_off: usize,
    alloc_len: usize,
    new_path: &str,
    file: &Path,
) -> Result<()> {
    if new_path.len() + 1 > alloc_len || abs_off + alloc_len > buf.len() {
        // should never happen – just log & skip
        tracing::debug!(
            "Patch skipped (bounds) at {} in {}",
            abs_off,
            file.display()
        );
        return Ok(());
    }

    // null‑padded copy
    buf[abs_off..abs_off + new_path.len()].copy_from_slice(new_path.as_bytes());
    buf[abs_off + new_path.len()..abs_off + alloc_len].fill(0);

    Ok(())
}

/// Writes the patched buffer to the original path atomically using a temporary file.
#[cfg(target_os = "macos")]
fn write_patched_buffer(original_path: &Path, buffer: &[u8]) -> Result<()> {
    // Get the directory containing the original file
    let dir = original_path.parent().ok_or_else(|| {
        SpsError::Generic(format!(
            "Cannot get parent directory for {}",
            original_path.display()
        ))
    })?;
    // Ensure the directory exists
    fs::create_dir_all(dir).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

    // Create a named temporary file in the same directory to facilitate atomic rename
    let mut temp_file = NamedTempFile::new_in(dir)?;
    debug!(
        "    Writing patched buffer ({} bytes) to temporary file: {:?}",
        buffer.len(),
        temp_file.path()
    );
    // Write the entire modified buffer to the temporary file
    temp_file.write_all(buffer)?;
    // Ensure data is flushed to the OS buffer
    temp_file.flush()?;
    // Attempt to sync data to the disk
    temp_file.as_file().sync_all()?; // Ensure data is physically written

    // Atomically replace the original file with the temporary file
    // persist() renames the temp file over the original path.
    temp_file.persist(original_path).map_err(|e| {
        // If persist fails, the temporary file might still exist.
        // The error 'e' contains both the temp file and the underlying IO error.
        error!(
            "    Failed to persist/rename temporary file over {}: {}",
            original_path.display(),
            e.error // Log the underlying IO error
        );
        // Return the IO error wrapped in our error type
        SpsError::Io(std::sync::Arc::new(e.error))
    })?;
    debug!(
        "    Atomically replaced {} with patched version",
        original_path.display()
    );
    Ok(())
}

/// Re-signs the binary using the `codesign` command-line tool.
/// This is typically necessary on Apple Silicon (aarch64) after modifying executables.
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn resign_binary(path: &Path) -> Result<()> {
    // Suppressed: debug!("Re-signing patched binary: {}", path.display());
    let status = StdCommand::new("codesign")
        .args([
            "-s",
            "-",
            "--force",
            "--preserve-metadata=identifier,entitlements",
        ])
        .arg(path)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status() // Execute the command and get its exit status
        .map_err(|e| {
            error!(
                "    Failed to execute codesign command for {}: {}",
                path.display(),
                e
            );
            SpsError::Io(std::sync::Arc::new(e))
        })?;
    if status.success() {
        // Suppressed: debug!("Successfully re-signed {}", path.display());
        Ok(())
    } else {
        error!(
            "    codesign command failed for {} with status: {}",
            path.display(),
            status
        );
        Err(SpsError::CodesignError(format!(
            "Failed to re-sign patched binary {}, it may not be executable. Exit status: {}",
            path.display(),
            status
        )))
    }
}

// No-op stub for resigning on non-Apple Silicon macOS (e.g., x86_64)
#[cfg(all(target_os = "macos", not(target_arch = "aarch64")))]
fn resign_binary(_path: &Path) -> Result<()> {
    // No re-signing typically needed on Intel Macs after ad-hoc patching
    Ok(())
}

// No-op stub for resigning Innovations on non-macOS platforms
#[cfg(not(target_os = "macos"))]
fn resign_binary(_path: &Path) -> Result<()> {
    // Resigning is a macOS concept
    Ok(())
}

// sps/src/cli/init.rs
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::Command as StdCommand;
use std::sync::Arc;

use clap::Args;
use colored::Colorize;
use sps_common::config::Config; // Assuming Config is correctly in sps_common
use sps_common::error::{Result as SpsResult, SpsError};
use tempfile;
use tracing::{debug, error, warn};

#[derive(Args, Debug)]
pub struct InitArgs {
    /// Force initialization even if /opt/sps appears to be an sps root already.
    #[arg(long)]
    pub force: bool,
}

impl InitArgs {
    pub async fn run(&self, config: &Config) -> SpsResult<()> {
        debug!(
            "Initializing sps environment at {}",
            config.sps_root().display()
        );

        let sps_root = config.sps_root();
        let marker_path = config.sps_root_marker_path();

        // 1. Initial Checks (as current user) - (No change from your existing logic)
        if sps_root.exists() {
            let is_empty = match fs::read_dir(sps_root) {
                Ok(mut entries) => entries.next().is_none(),
                Err(_) => false, // If we can't read it, assume not empty or not accessible
            };

            if marker_path.exists() && !self.force {
                debug!(
                    "{} already exists. sps appears to be initialized. Use --force to re-initialize.",
                    marker_path.display()
                );
                return Ok(());
            }
            if !self.force && !is_empty && !marker_path.exists() {
                warn!(
                    "Directory {} exists but does not appear to be an sps root (missing marker {}).",
                    sps_root.display(),
                    marker_path.file_name().unwrap_or_default().to_string_lossy()
                );
                warn!(
                    "Run with --force to initialize anyway (this might overwrite existing data or change permissions)."
                );
                return Err(SpsError::Config(format!(
                    "{} exists but is not a recognized sps root. Aborting.",
                    sps_root.display()
                )));
            }
            if self.force {
                debug!(
                    "--force specified. Proceeding with initialization in {}.",
                    sps_root.display()
                );
            } else if is_empty {
                debug!(
                    "Directory {} exists but is empty. Proceeding with initialization.",
                    sps_root.display()
                );
            }
        }

        // 2. Privileged Operations
        let current_user_name = std::env::var("USER")
            .or_else(|_| std::env::var("LOGNAME"))
            .map_err(|_| {
                SpsError::Generic(
                    "Failed to get current username from USER or LOGNAME environment variables."
                        .to_string(),
                )
            })?;

        let target_group_name = if cfg!(target_os = "macos") {
            "admin" // Standard admin group on macOS
        } else {
            // For Linux, 'staff' might not exist or be appropriate.
            // Often, the user's primary group is used, or a dedicated 'brew' group.
            // For simplicity, let's try to use the current user's name as the group too,
            // which works if the user has a group with the same name.
            // A more robust Linux solution might involve checking for 'staff' or other common
            // groups.
            &current_user_name
        };

        debug!(
            "Will attempt to set ownership of sps-managed directories under {} to {}:{}",
            sps_root.display(),
            current_user_name,
            target_group_name
        );

        println!(
            "{}",
            format!(
                "sps may require sudo to create directories and set permissions in {}.",
                sps_root.display()
            )
            .yellow()
        );

        // Define directories sps needs to ensure exist and can manage.
        // These are derived from your Config struct.
        let dirs_to_create_and_manage: Vec<PathBuf> = vec![
            config.sps_root().to_path_buf(), // The root itself
            config.bin_dir(),
            config.cellar_dir(),
            config.cask_room_dir(),
            config.cask_store_dir(), // sps-specific
            config.opt_dir(),
            config.taps_dir(),  // This is now sps_root/Library/Taps
            config.cache_dir(), // sps-specific (e.g., sps_root/sps_cache)
            config.logs_dir(),  // sps-specific (e.g., sps_root/sps_logs)
            config.tmp_dir(),
            config.state_dir(),
            config
                .man_base_dir()
                .parent()
                .unwrap_or(sps_root)
                .to_path_buf(), // share
            config.man_base_dir(), // share/man
            config.sps_root().join("etc"),
            config.sps_root().join("include"),
            config.sps_root().join("lib"),
            config.sps_root().join("share/doc"),
        ];

        // Create directories with mkdir -p (non-destructive)
        for dir_path in &dirs_to_create_and_manage {
            // Only create if it doesn't exist to avoid unnecessary sudo calls if already present
            if !dir_path.exists() {
                debug!(
                    "Ensuring directory exists with sudo: {}",
                    dir_path.display()
                );
                run_sudo_command("mkdir", &["-p", &dir_path.to_string_lossy()])?;
            } else {
                debug!(
                    "Directory already exists, skipping mkdir: {}",
                    dir_path.display()
                );
            }
        }

        // Create the marker file (non-destructive to other content)
        debug!(
            "Creating/updating marker file with sudo: {}",
            marker_path.display()
        );
        let marker_content = "sps root directory version 1";
        // Using a temporary file for sudo tee to avoid permission issues with direct pipe
        let temp_marker_file =
            tempfile::NamedTempFile::new().map_err(|e| SpsError::Io(Arc::new(e)))?;
        fs::write(temp_marker_file.path(), marker_content)
            .map_err(|e| SpsError::Io(Arc::new(e)))?;
        run_sudo_command(
            "cp",
            &[
                temp_marker_file.path().to_str().unwrap(),
                marker_path.to_str().unwrap(),
            ],
        )?;

        #[cfg(unix)]
        {
            // More targeted chown and chmod
            debug!(
                "Setting ownership and permissions for sps-managed directories under {}...",
                sps_root.display()
            );

            // Chown/Chmod the top-level sps_root directory itself (non-recursively for chmod
            // initially) This is important if sps_root is /opt/sps and was just created
            // by root. If sps_root is /opt/homebrew, this ensures the current user can
            // at least manage it.
            run_sudo_command(
                "chown",
                &[
                    &format!("{current_user_name}:{target_group_name}"),
                    &sps_root.to_string_lossy(),
                ],
            )?;
            run_sudo_command("chmod", &["ug=rwx,o=rx", &sps_root.to_string_lossy()])?; // 755 for the root

            // For specific subdirectories that sps actively manages and writes into frequently,
            // ensure they are owned by the user and have appropriate permissions.
            // We apply this recursively to sps-specific dirs and key shared dirs.
            let dirs_for_recursive_chown_chmod: Vec<PathBuf> = vec![
                config.cellar_dir(),
                config.cask_room_dir(),
                config.cask_store_dir(), // sps-specific, definitely needs full user control
                config.opt_dir(),
                config.taps_dir(),
                config.cache_dir(), // sps-specific
                config.logs_dir(),  // sps-specific
                config.tmp_dir(),
                config.state_dir(),
                // bin, lib, include, share, etc are often symlink farms.
                // The top-level of these should be writable by the user to create symlinks.
                // The actual kegs in Cellar will have their own permissions.
                config.bin_dir(),
                config.sps_root().join("lib"),
                config.sps_root().join("include"),
                config.sps_root().join("share"),
                config.sps_root().join("etc"),
            ];

            for dir_path in dirs_for_recursive_chown_chmod {
                if dir_path.exists() {
                    // Only operate on existing directories
                    debug!("Setting ownership (recursive) for: {}", dir_path.display());
                    run_sudo_command(
                        "chown",
                        &[
                            "-R",
                            &format!("{current_user_name}:{target_group_name}"),
                            &dir_path.to_string_lossy(),
                        ],
                    )?;

                    debug!(
                        "Setting permissions (recursive ug=rwX,o=rX) for: {}",
                        dir_path.display()
                    );
                    run_sudo_command("chmod", &["-R", "ug=rwX,o=rX", &dir_path.to_string_lossy()])?;
                } else {
                    warn!(
                        "Directory {} was expected but not found for chown/chmod. Marker: {}",
                        dir_path.display(),
                        marker_path.display()
                    );
                }
            }

            // Ensure bin is executable by all
            if config.bin_dir().exists() {
                debug!(
                    "Ensuring execute permissions for bin_dir: {}",
                    config.bin_dir().display()
                );
                run_sudo_command("chmod", &["a+x", &config.bin_dir().to_string_lossy()])?;
                // Also ensure contents of bin (wrappers, symlinks) are executable if they weren't
                // caught by -R ug=rwX This might be redundant if -R ug=rwX
                // correctly sets X for existing executables, but explicit `chmod
                // a+x` on individual files might be needed if they are newly created by sps.
                // For now, relying on the recursive chmod and the a+x on the bin_dir itself.
            }

            // Debug listing (optional, can be verbose)
            if tracing::enabled!(tracing::Level::DEBUG) {
                debug!("Listing {} after permission changes:", sps_root.display());
                let ls_output_root = StdCommand::new("ls").arg("-ld").arg(sps_root).output();
                if let Ok(out) = ls_output_root {
                    debug!(
                        "ls -ld {}: \nSTDOUT: {}\nSTDERR: {}",
                        sps_root.display(),
                        String::from_utf8_lossy(&out.stdout),
                        String::from_utf8_lossy(&out.stderr)
                    );
                }
                for dir_path in &dirs_to_create_and_manage {
                    if dir_path.exists() && dir_path != sps_root {
                        let ls_output_sub = StdCommand::new("ls").arg("-ld").arg(dir_path).output();
                        if let Ok(out) = ls_output_sub {
                            debug!(
                                "ls -ld {}: \nSTDOUT: {}\nSTDERR: {}",
                                dir_path.display(),
                                String::from_utf8_lossy(&out.stdout),
                                String::from_utf8_lossy(&out.stderr)
                            );
                        }
                    }
                }
            }
        }

        // 3. User-Specific PATH Configuration (runs as the original user) - (No change from your
        //    existing logic)
        if let Err(e) = configure_shell_path(config, &current_user_name) {
            warn!(
                "Could not fully configure shell PATH: {}. Manual configuration might be needed.",
                e
            );
            print_manual_path_instructions(&config.bin_dir().to_string_lossy());
        }

        debug!(
            "{} {}",
            "Successfully initialized sps environment at".green(),
            config.sps_root().display().to_string().green()
        );
        Ok(())
    }
}

// run_sudo_command helper (no change from your existing logic)
fn run_sudo_command(command: &str, args: &[&str]) -> SpsResult<()> {
    debug!("Running sudo {} {:?}", command, args);
    let output = StdCommand::new("sudo")
        .arg(command)
        .args(args)
        .output()
        .map_err(|e| SpsError::Io(Arc::new(e)))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        error!(
            "sudo {} {:?} failed ({}):\nStdout: {}\nStderr: {}",
            command,
            args,
            output.status,
            stdout.trim(),
            stderr.trim()
        );
        Err(SpsError::Generic(format!(
            "Failed to execute `sudo {} {:?}`: {}",
            command,
            args,
            stderr.trim()
        )))
    } else {
        Ok(())
    }
}

// configure_shell_path helper (no change from your existing logic)
fn configure_shell_path(config: &Config, current_user_name_for_log: &str) -> SpsResult<()> {
    debug!("Attempting to configure your shell for sps PATH...");

    let sps_bin_path_str = config.bin_dir().to_string_lossy().into_owned();
    let home_dir = config.home_dir();
    if home_dir == PathBuf::from("/") && current_user_name_for_log != "root" {
        warn!(
            "Could not reliably determine your home directory (got '/'). Please add {} to your PATH manually for user {}.",
            sps_bin_path_str, current_user_name_for_log
        );
        print_manual_path_instructions(&sps_bin_path_str);
        return Ok(());
    }

    let shell_path_env = std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string());
    let mut config_files_updated: Vec<String> = Vec::new();
    let mut path_seems_configured = false;

    let sps_path_line_zsh_bash = format!("export PATH=\"{sps_bin_path_str}:$PATH\"");
    let sps_path_line_fish = format!("fish_add_path -P \"{sps_bin_path_str}\"");

    if shell_path_env.contains("zsh") {
        let zshrc_path = home_dir.join(".zshrc");
        if update_shell_config(
            &zshrc_path,
            &sps_path_line_zsh_bash,
            &sps_bin_path_str,
            "Zsh",
            false,
        )? {
            config_files_updated.push(zshrc_path.display().to_string());
        } else if line_exists_in_file(&zshrc_path, &sps_bin_path_str)? {
            path_seems_configured = true;
        }
    } else if shell_path_env.contains("bash") {
        let bashrc_path = home_dir.join(".bashrc");
        let bash_profile_path = home_dir.join(".bash_profile");
        let profile_path = home_dir.join(".profile");

        let mut bash_updated_by_sps = false;
        if update_shell_config(
            &bashrc_path,
            &sps_path_line_zsh_bash,
            &sps_bin_path_str,
            "Bash (.bashrc)",
            false,
        )? {
            config_files_updated.push(bashrc_path.display().to_string());
            bash_updated_by_sps = true;
            if bash_profile_path.exists() {
                ensure_profile_sources_rc(&bash_profile_path, &bashrc_path, "Bash (.bash_profile)");
            } else if profile_path.exists() {
                ensure_profile_sources_rc(&profile_path, &bashrc_path, "Bash (.profile)");
            } else {
                debug!("Neither .bash_profile nor .profile found. Creating .bash_profile to source .bashrc.");
                ensure_profile_sources_rc(&bash_profile_path, &bashrc_path, "Bash (.bash_profile)");
            }
        } else if update_shell_config(
            &bash_profile_path,
            &sps_path_line_zsh_bash,
            &sps_bin_path_str,
            "Bash (.bash_profile)",
            false,
        )? {
            config_files_updated.push(bash_profile_path.display().to_string());
            bash_updated_by_sps = true;
        } else if update_shell_config(
            &profile_path,
            &sps_path_line_zsh_bash,
            &sps_bin_path_str,
            "Bash (.profile)",
            false,
        )? {
            config_files_updated.push(profile_path.display().to_string());
            bash_updated_by_sps = true;
        }

        if !bash_updated_by_sps
            && (line_exists_in_file(&bashrc_path, &sps_bin_path_str)?
                || line_exists_in_file(&bash_profile_path, &sps_bin_path_str)?
                || line_exists_in_file(&profile_path, &sps_bin_path_str)?)
        {
            path_seems_configured = true;
        }
    } else if shell_path_env.contains("fish") {
        let fish_config_dir = home_dir.join(".config/fish");
        if !fish_config_dir.exists() {
            if let Err(e) = fs::create_dir_all(&fish_config_dir) {
                warn!(
                    "Could not create Fish config directory {}: {}",
                    fish_config_dir.display(),
                    e
                );
            }
        }
        if fish_config_dir.exists() {
            let fish_config_path = fish_config_dir.join("config.fish");
            if update_shell_config(
                &fish_config_path,
                &sps_path_line_fish,
                &sps_bin_path_str,
                "Fish",
                true,
            )? {
                config_files_updated.push(fish_config_path.display().to_string());
            } else if line_exists_in_file(&fish_config_path, &sps_bin_path_str)? {
                path_seems_configured = true;
            }
        }
    } else {
        warn!(
            "Unsupported shell for automatic PATH configuration: {}. Please add {} to your PATH manually.",
            shell_path_env, sps_bin_path_str
        );
        print_manual_path_instructions(&sps_bin_path_str);
        return Ok(());
    }

    if !config_files_updated.is_empty() {
        println!(
            "{} {} has been added to your PATH by modifying: {}",
            "sps".cyan(),
            sps_bin_path_str.cyan(),
            config_files_updated.join(", ").white()
        );
        println!(
            "{}",
            "Please open a new terminal session or source your shell configuration file for the changes to take effect."
                .yellow()
        );
        if shell_path_env.contains("zsh") {
            println!("  Run: {}", "source ~/.zshrc".green());
        }
        if shell_path_env.contains("bash") {
            println!(
                "  Run: {} (or {} or {})",
                "source ~/.bashrc".green(),
                "source ~/.bash_profile".green(),
                "source ~/.profile".green()
            );
        }
        if shell_path_env.contains("fish") {
            println!(
                "  Run (usually not needed for fish_add_path, but won't hurt): {}",
                "source ~/.config/fish/config.fish".green()
            );
        }
    } else if path_seems_configured {
        debug!("sps path ({}) is likely already configured for your shell ({}). No configuration files were modified.", sps_bin_path_str.cyan(), shell_path_env.yellow());
    } else if !shell_path_env.is_empty() && shell_path_env != "unknown" {
        warn!("Could not automatically update PATH for your shell: {}. Please add {} to your PATH manually.", shell_path_env.yellow(), sps_bin_path_str.cyan());
        print_manual_path_instructions(&sps_bin_path_str);
    }
    Ok(())
}

// print_manual_path_instructions helper (no change from your existing logic)
fn print_manual_path_instructions(sps_bin_path_str: &str) {
    println!("\n{} To use sps commands and installed packages directly, please add the following line to your shell configuration file:", "Action Required:".yellow().bold());
    println!("  (e.g., ~/.zshrc, ~/.bashrc, ~/.config/fish/config.fish)");
    println!("\n  For Zsh or Bash:");
    println!(
        "    {}",
        format!("export PATH=\"{sps_bin_path_str}:$PATH\"").green()
    );
    println!("\n  For Fish shell:");
    println!(
        "    {}",
        format!("fish_add_path -P \"{sps_bin_path_str}\"").green()
    );
    println!(
        "\nThen, open a new terminal or run: {}",
        "source <your_shell_config_file>".green()
    );
}

// line_exists_in_file helper (no change from your existing logic)
fn line_exists_in_file(file_path: &Path, sps_bin_path_str: &str) -> SpsResult<bool> {
    if !file_path.exists() {
        return Ok(false);
    }
    let file = File::open(file_path).map_err(|e| SpsError::Io(Arc::new(e)))?;
    let reader = BufReader::new(file);
    let escaped_sps_bin_path = regex::escape(sps_bin_path_str);
    // Regex to find lines that configure PATH, trying to be robust for different shells
    // It looks for lines that set PATH or fish_user_paths and include the sps_bin_path_str
    // while trying to avoid commented out lines.
    let pattern = format!(
        r#"(?m)^\s*[^#]*\b(?:PATH\s*=|export\s+PATH\s*=|set\s*(?:-gx\s*|-U\s*)?\s*fish_user_paths\b|fish_add_path\s*(?:-P\s*|-p\s*)?)?["']?.*{escaped_sps_bin_path}.*["']?"#
    );
    let search_pattern_regex = regex::Regex::new(&pattern)
        .map_err(|e| SpsError::Generic(format!("Failed to compile regex for PATH check: {e}")))?;

    for line_result in reader.lines() {
        let line = line_result.map_err(|e| SpsError::Io(Arc::new(e)))?;
        if search_pattern_regex.is_match(&line) {
            debug!(
                "Found sps PATH ({}) in {}: {}",
                sps_bin_path_str,
                file_path.display(),
                line.trim()
            );
            return Ok(true);
        }
    }
    Ok(false)
}

// update_shell_config helper (no change from your existing logic)
fn update_shell_config(
    config_path: &PathBuf,
    line_to_add: &str,
    sps_bin_path_str: &str,
    shell_name_for_log: &str,
    is_fish_shell: bool,
) -> SpsResult<bool> {
    let sps_comment_tag_start = "# SPS Path Management Start";
    let sps_comment_tag_end = "# SPS Path Management End";

    if config_path.exists() {
        match line_exists_in_file(config_path, sps_bin_path_str) {
            Ok(true) => {
                debug!(
                    "sps path ({}) already configured or managed in {} ({}). Skipping modification.",
                    sps_bin_path_str,
                    config_path.display(),
                    shell_name_for_log
                );
                return Ok(false); // Path already seems configured
            }
            Ok(false) => { /* Proceed to add */ }
            Err(e) => {
                warn!(
                    "Could not reliably check existing configuration in {} ({}): {}. Attempting to add PATH.",
                    config_path.display(),
                    shell_name_for_log,
                    e
                );
                // Proceed with caution, might add duplicate if check failed but line exists
            }
        }
    }

    debug!(
        "Adding sps PATH to {} ({})",
        config_path.display(),
        shell_name_for_log
    );

    // Ensure parent directory exists
    if let Some(parent_dir) = config_path.parent() {
        if !parent_dir.exists() {
            fs::create_dir_all(parent_dir).map_err(|e| {
                SpsError::Io(Arc::new(std::io::Error::new(
                    e.kind(),
                    format!(
                        "Failed to create parent directory {}: {}",
                        parent_dir.display(),
                        e
                    ),
                )))
            })?;
        }
    }

    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open(config_path)
        .map_err(|e| {
            let msg = format!(
                "Could not open/create shell config {} ({}): {}. Please add {} to your PATH manually.",
                config_path.display(), shell_name_for_log, e, sps_bin_path_str
            );
            error!("{}", msg);
            SpsError::Io(Arc::new(std::io::Error::new(e.kind(), msg)))
        })?;

    // Construct the block to add, ensuring it's idempotent for fish
    let block_to_add = if is_fish_shell {
        format!(
            "\n{sps_comment_tag_start}\n# Add sps to PATH if not already present\nif not contains \"{sps_bin_path_str}\" $fish_user_paths\n    {line_to_add}\nend\n{sps_comment_tag_end}\n"
        )
    } else {
        format!("\n{sps_comment_tag_start}\n{line_to_add}\n{sps_comment_tag_end}\n")
    };

    if let Err(e) = writeln!(file, "{block_to_add}") {
        warn!(
            "Failed to write to shell config {} ({}): {}",
            config_path.display(),
            shell_name_for_log,
            e
        );
        Ok(false) // Indicate that update was not successful
    } else {
        debug!(
            "Successfully updated {} ({}) with sps PATH.",
            config_path.display(),
            shell_name_for_log
        );
        Ok(true) // Indicate successful update
    }
}

// ensure_profile_sources_rc helper (no change from your existing logic)
fn ensure_profile_sources_rc(profile_path: &PathBuf, rc_path: &Path, shell_name_for_log: &str) {
    let rc_path_str = rc_path.to_string_lossy();
    // Regex to check if the profile file already sources the rc file.
    // Looks for lines like:
    // . /path/to/.bashrc
    // source /path/to/.bashrc
    // [ -f /path/to/.bashrc ] && . /path/to/.bashrc (and similar variants)
    let source_check_pattern = format!(
        r#"(?m)^\s*[^#]*(\.|source|\bsource\b)\s+["']?{}["']?"#, /* More general source command
                                                                  * matching */
        regex::escape(&rc_path_str)
    );
    let source_check_regex = match regex::Regex::new(&source_check_pattern) {
        Ok(re) => re,
        Err(e) => {
            warn!("Failed to compile regex for sourcing check: {}. Skipping ensure_profile_sources_rc for {}", e, profile_path.display());
            return;
        }
    };

    if profile_path.exists() {
        match fs::read_to_string(profile_path) {
            Ok(existing_content) => {
                if source_check_regex.is_match(&existing_content) {
                    debug!(
                        "{} ({}) already sources {}.",
                        profile_path.display(),
                        shell_name_for_log,
                        rc_path.display()
                    );
                    return; // Already configured
                }
            }
            Err(e) => {
                warn!(
                    "Could not read {} to check if it sources {}: {}. Will attempt to append.",
                    profile_path.display(),
                    rc_path.display(),
                    e
                );
            }
        }
    }

    // Block to add to .bash_profile or .profile to source .bashrc
    let source_block_to_add = format!(
        "\n# Source {rc_filename} if it exists and is readable\nif [ -f \"{rc_path_str}\" ] && [ -r \"{rc_path_str}\" ]; then\n    . \"{rc_path_str}\"\nfi\n",
        rc_filename = rc_path.file_name().unwrap_or_default().to_string_lossy(),
        rc_path_str = rc_path_str
    );

    debug!(
        "Attempting to ensure {} ({}) sources {}",
        profile_path.display(),
        shell_name_for_log,
        rc_path.display()
    );

    if let Some(parent_dir) = profile_path.parent() {
        if !parent_dir.exists() {
            if let Err(e) = fs::create_dir_all(parent_dir) {
                warn!(
                    "Failed to create parent directory for {}: {}",
                    profile_path.display(),
                    e
                );
                return; // Cannot proceed if parent dir creation fails
            }
        }
    }

    match OpenOptions::new()
        .append(true)
        .create(true)
        .open(profile_path)
    {
        Ok(mut file) => {
            if let Err(e) = writeln!(file, "{source_block_to_add}") {
                warn!(
                    "Failed to write to {} ({}): {}",
                    profile_path.display(),
                    shell_name_for_log,
                    e
                );
            } else {
                debug!(
                    "Updated {} ({}) to source {}.",
                    profile_path.display(),
                    shell_name_for_log,
                    rc_path.display()
                );
            }
        }
        Err(e) => {
            warn!(
                "Could not open or create {} ({}) for updating: {}",
                profile_path.display(),
                shell_name_for_log,
                e
            );
        }
    }
}

// sps-common/src/keg.rs
use std::fs;
use std::path::PathBuf;

// Corrected tracing imports: added error, removed unused debug
use tracing::{debug, error, warn};

use super::config::Config;
use super::error::{Result, SpsError};

/// Represents information about an installed package (Keg).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledKeg {
    pub name: String,
    pub version_str: String,
    pub path: PathBuf,
}

/// Manages querying installed packages in the Cellar.
#[derive(Debug)]
pub struct KegRegistry {
    config: Config,
}

impl KegRegistry {
    pub fn new(config: Config) -> Self {
        Self { config }
    }

    fn formula_cellar_path(&self, name: &str) -> PathBuf {
        self.config.cellar_dir().join(name)
    }

    pub fn get_opt_path(&self, name: &str) -> PathBuf {
        self.config.opt_dir().join(name)
    }

    pub fn get_installed_keg(&self, name: &str) -> Result<Option<InstalledKeg>> {
        let formula_dir = self.formula_cellar_path(name);
        debug!(
            "[KEG_REGISTRY:{}] get_installed_keg: Checking for formula '{}'. Path to check: {}",
            name,
            name,
            formula_dir.display()
        );

        if !formula_dir.is_dir() {
            debug!("[KEG_REGISTRY:{}] get_installed_keg: Formula directory '{}' NOT FOUND or not a directory. Returning None.", name, formula_dir.display());
            return Ok(None);
        }

        let mut latest_keg: Option<InstalledKeg> = None;
        debug!(
            "[KEG_REGISTRY:{}] get_installed_keg: Reading entries in formula directory '{}'",
            name,
            formula_dir.display()
        );

        match fs::read_dir(&formula_dir) {
            Ok(entries) => {
                for entry_result in entries {
                    match entry_result {
                        Ok(entry) => {
                            let path = entry.path();
                            debug!(
                                "[KEG_REGISTRY:{}] get_installed_keg: Examining entry '{}'",
                                name,
                                path.display()
                            );

                            if path.is_dir() {
                                if let Some(version_str_full) =
                                    path.file_name().and_then(|n| n.to_str())
                                {
                                    debug!("[KEG_REGISTRY:{}] get_installed_keg: Entry is a directory with name: {}", name, version_str_full);

                                    let current_keg_candidate = InstalledKeg {
                                        name: name.to_string(),
                                        version_str: version_str_full.to_string(),
                                        path: path.clone(),
                                    };

                                    match latest_keg {
                                        Some(ref current_latest) => {
                                            // Compare &str with &str
                                            if version_str_full
                                                > current_latest.version_str.as_str()
                                            {
                                                debug!("[KEG_REGISTRY:{}] get_installed_keg: Updating latest keg (lexicographical) to: {}", name, path.display());
                                                latest_keg = Some(current_keg_candidate);
                                            }
                                        }
                                        None => {
                                            debug!("[KEG_REGISTRY:{}] get_installed_keg: Setting first found keg as latest: {}", name, path.display());
                                            latest_keg = Some(current_keg_candidate);
                                        }
                                    }
                                } else {
                                    debug!("[KEG_REGISTRY:{}] get_installed_keg: Could not get filename as string for path '{}'", name, path.display());
                                }
                            } else {
                                debug!("[KEG_REGISTRY:{}] get_installed_keg: Entry '{}' is not a directory.", name, path.display());
                            }
                        }
                        Err(e) => {
                            warn!("[KEG_REGISTRY:{}] get_installed_keg: Error reading a directory entry in '{}': {}. Skipping entry.", name, formula_dir.display(), e);
                        }
                    }
                }
            }
            Err(e) => {
                // Corrected macro usage
                error!("[KEG_REGISTRY:{}] get_installed_keg: Failed to read_dir for formula directory '{}' (error: {}). Returning error.", name, formula_dir.display(), e);
                return Err(SpsError::Io(std::sync::Arc::new(e)));
            }
        }

        if let Some(keg) = &latest_keg {
            // Corrected format string arguments
            debug!("[KEG_REGISTRY:{}] get_installed_keg: For formula '{}', latest keg found: path={}, version_str={}", name, name, keg.path.display(), keg.version_str);
        } else {
            // Corrected format string arguments
            debug!("[KEG_REGISTRY:{}] get_installed_keg: For formula '{}', no installable keg version found in directory '{}'.", name, name, formula_dir.display());
        }
        Ok(latest_keg)
    }

    pub fn list_installed_kegs(&self) -> Result<Vec<InstalledKeg>> {
        let mut installed_kegs = Vec::new();
        let cellar_dir = self.cellar_path();
        debug!(
            "[KEG_REGISTRY] list_installed_kegs: Scanning cellar: {}",
            cellar_dir.display()
        );

        if !cellar_dir.is_dir() {
            debug!("[KEG_REGISTRY] list_installed_kegs: Cellar directory NOT FOUND. Returning empty list.");
            return Ok(installed_kegs);
        }

        for formula_entry_res in fs::read_dir(cellar_dir)? {
            let formula_entry = match formula_entry_res {
                Ok(fe) => fe,
                Err(e) => {
                    warn!("[KEG_REGISTRY] list_installed_kegs: Error reading entry in cellar: {}. Skipping.", e);
                    continue;
                }
            };
            let formula_path = formula_entry.path();
            debug!(
                "[KEG_REGISTRY] list_installed_kegs: Examining formula path: {}",
                formula_path.display()
            );

            if formula_path.is_dir() {
                if let Some(formula_name) = formula_path.file_name().and_then(|n| n.to_str()) {
                    debug!(
                        "[KEG_REGISTRY] list_installed_kegs: Found formula directory: {}",
                        formula_name
                    );
                    match fs::read_dir(&formula_path) {
                        Ok(version_entries) => {
                            for version_entry_res in version_entries {
                                let version_entry = match version_entry_res {
                                    Ok(ve) => ve,
                                    Err(e) => {
                                        warn!("[KEG_REGISTRY:{}] list_installed_kegs: Error reading version entry in '{}': {}. Skipping.", formula_name, formula_path.display(), e);
                                        continue;
                                    }
                                };
                                let version_path = version_entry.path();
                                debug!("[KEG_REGISTRY:{}] list_installed_kegs: Examining version path: {}", formula_name, version_path.display());

                                if version_path.is_dir() {
                                    if let Some(version_str_full) =
                                        version_path.file_name().and_then(|n| n.to_str())
                                    {
                                        debug!("[KEG_REGISTRY:{}] list_installed_kegs: Found version directory '{}' with name: {}", formula_name, version_path.display(), version_str_full);
                                        installed_kegs.push(InstalledKeg {
                                            name: formula_name.to_string(),
                                            version_str: version_str_full.to_string(),
                                            path: version_path.clone(),
                                        });
                                    } else {
                                        debug!("[KEG_REGISTRY:{}] list_installed_kegs: Could not get filename for version path {}", formula_name, version_path.display());
                                    }
                                } else {
                                    debug!("[KEG_REGISTRY:{}] list_installed_kegs: Version path {} is not a directory.", formula_name, version_path.display());
                                }
                            }
                        }
                        Err(e) => {
                            warn!("[KEG_REGISTRY:{}] list_installed_kegs: Failed to read_dir for formula versions in '{}': {}.", formula_name, formula_path.display(), e);
                        }
                    }
                } else {
                    debug!("[KEG_REGISTRY] list_installed_kegs: Could not get filename for formula path {}", formula_path.display());
                }
            } else {
                debug!(
                    "[KEG_REGISTRY] list_installed_kegs: Formula path {} is not a directory.",
                    formula_path.display()
                );
            }
        }
        debug!(
            "[KEG_REGISTRY] list_installed_kegs: Found {} total installed keg versions.",
            installed_kegs.len()
        );
        Ok(installed_kegs)
    }

    pub fn cellar_path(&self) -> PathBuf {
        self.config.cellar_dir()
    }

    pub fn get_keg_path(&self, name: &str, version_str_raw: &str) -> PathBuf {
        self.formula_cellar_path(name).join(version_str_raw)
    }
}

// ===== sps-core/src/build/formula/mod.rs =====
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::formula::Formula;
use tracing::{debug, error};

// Declare submodules
pub mod exec;
pub mod link;
pub mod macho;

/// Download formula resources from the internet asynchronously.
pub async fn download_formula(
    formula: &Formula,
    config: &Config,
    client: &reqwest::Client,
) -> Result<PathBuf> {
    if has_bottle_for_current_platform(formula) {
        exec::download_bottle(formula, config, client).await
    } else {
        Err(SpsError::Generic(format!(
            "No bottle available for {} on this platform",
            formula.name()
        )))
    }
}

/// Checks if a suitable bottle exists for the current platform, considering fallbacks.
pub fn has_bottle_for_current_platform(formula: &Formula) -> bool {
    let result = crate::install::bottle::exec::get_bottle_for_platform(formula);
    debug!(
        "has_bottle_for_current_platform check for '{}': {:?}",
        formula.name(),
        result.is_ok()
    );
    if let Err(e) = &result {
        debug!("Reason for no bottle: {}", e);
    }
    result.is_ok()
}

// *** Updated get_current_platform function ***
fn get_current_platform() -> String {
    if cfg!(target_os = "macos") {
        let arch = if std::env::consts::ARCH == "aarch64" {
            "arm64"
        } else if std::env::consts::ARCH == "x86_64" {
            "x86_64"
        } else {
            std::env::consts::ARCH
        };

        debug!("Attempting to determine macOS version using /usr/bin/sw_vers -productVersion");
        match Command::new("/usr/bin/sw_vers")
            .arg("-productVersion")
            .output()
        {
            Ok(output) => {
                let stdout = String::from_utf8_lossy(&output.stdout);
                let stderr = String::from_utf8_lossy(&output.stderr);
                debug!("sw_vers status: {}", output.status);
                if !output.status.success() || !stderr.trim().is_empty() {
                    debug!("sw_vers stdout:\n{}", stdout);
                    if !stderr.trim().is_empty() {
                        debug!("sw_vers stderr:\n{}", stderr);
                    }
                }

                if output.status.success() {
                    let version_str = stdout.trim();
                    if !version_str.is_empty() {
                        debug!("Extracted version string: {}", version_str);
                        let os_name = match version_str.split('.').next() {
                            Some("15") => "sequoia",
                            Some("14") => "sonoma",
                            Some("13") => "ventura",
                            Some("12") => "monterey",
                            Some("11") => "big_sur",
                            Some("10") => match version_str.split('.').nth(1) {
                                Some("15") => "catalina",
                                Some("14") => "mojave",
                                _ => {
                                    debug!(
                                        "Unrecognized legacy macOS 10.x version: {}",
                                        version_str
                                    );
                                    "unknown_macos"
                                }
                            },
                            _ => {
                                debug!("Unrecognized macOS major version: {}", version_str);
                                "unknown_macos"
                            }
                        };

                        if os_name != "unknown_macos" {
                            let platform_tag = if arch == "arm64" {
                                format!("{arch}_{os_name}")
                            } else {
                                os_name.to_string()
                            };
                            debug!("Determined platform tag: {}", platform_tag);
                            return platform_tag;
                        }
                    } else {
                        error!("sw_vers -productVersion output was empty.");
                    }
                } else {
                    error!(
                        "sw_vers -productVersion command failed with status: {}. Stderr: {}",
                        output.status,
                        stderr.trim()
                    );
                }
            }
            Err(e) => {
                error!("Failed to execute /usr/bin/sw_vers -productVersion: {}", e);
            }
        }

        error!("!!! FAILED TO DETECT MACOS VERSION VIA SW_VERS !!!");
        debug!("Using UNRELIABLE fallback platform detection. Bottle selection may be incorrect.");
        if arch == "arm64" {
            debug!("Falling back to platform tag: arm64_monterey");
            "arm64_monterey".to_string()
        } else {
            debug!("Falling back to platform tag: monterey");
            "monterey".to_string()
        }
    } else if cfg!(target_os = "linux") {
        if std::env::consts::ARCH == "aarch64" {
            "arm64_linux".to_string()
        } else if std::env::consts::ARCH == "x86_64" {
            "x86_64_linux".to_string()
        } else {
            "unknown".to_string()
        }
    } else {
        debug!(
            "Could not determine platform tag for OS: {}",
            std::env::consts::OS
        );
        "unknown".to_string()
    }
}

// REMOVED: get_cellar_path (now in Config)

// --- get_formula_cellar_path uses Config ---
// Parameter changed from formula: &Formula to formula_name: &str
// Parameter changed from config: &Config to cellar_path: &Path for consistency where Config isn't
// fully available If Config *is* available, call config.formula_cellar_dir(formula.name()) instead.
// **Keeping original signature for now where Config might not be easily passed**
pub fn get_formula_cellar_path(formula: &Formula, config: &Config) -> PathBuf {
    // Use Config method
    config.formula_cellar_dir(formula.name())
}

// --- write_receipt (unchanged) ---
pub fn write_receipt(
    formula: &Formula,
    install_dir: &Path,
    installation_type: &str, // "bottle" or "source"
) -> Result<()> {
    let receipt_path = install_dir.join("INSTALL_RECEIPT.json");
    let receipt_file = File::create(&receipt_path);
    let mut receipt_file = match receipt_file {
        Ok(file) => file,
        Err(e) => {
            error!(
                "Failed to create receipt file at {}: {}",
                receipt_path.display(),
                e
            );
            return Err(SpsError::Io(std::sync::Arc::new(e)));
        }
    };

    let resources_result = formula.resources();
    let resources_installed = match resources_result {
        Ok(res) => res.iter().map(|r| r.name.clone()).collect::<Vec<_>>(),
        Err(_) => {
            debug!(
                "Could not retrieve resources for formula {} when writing receipt.",
                formula.name
            );
            vec![]
        }
    };

    let timestamp = chrono::Utc::now().to_rfc3339();

    let receipt = serde_json::json!({
        "name": formula.name, "version": formula.version_str_full(), "time": timestamp,
        "source": { "type": "api", "url": formula.url, },
        "built_on": {
            "os": std::env::consts::OS, "arch": std::env::consts::ARCH,
            "platform_tag": get_current_platform(),
         },
        "installation_type": installation_type,
        "resources_installed": resources_installed,
    });

    let receipt_json = match serde_json::to_string_pretty(&receipt) {
        Ok(json) => json,
        Err(e) => {
            error!(
                "Failed to serialize receipt JSON for {}: {}",
                formula.name, e
            );
            return Err(SpsError::Json(std::sync::Arc::new(e)));
        }
    };

    if let Err(e) = receipt_file.write_all(receipt_json.as_bytes()) {
        error!("Failed to write receipt file for {}: {}", formula.name, e);
        return Err(SpsError::Io(std::sync::Arc::new(e)));
    }

    Ok(())
}

// --- Re-exports (unchanged) ---
pub use exec::install_bottle;
pub use link::link_formula_artifacts;

// Path: sps-core/src/install/extract.rs
use std::collections::HashSet;
use std::fs::{self, File};
use std::io::{self, Read, Seek};
use std::path::{Component, Path, PathBuf};
use std::process::Command;

use bzip2::read::BzDecoder;
use flate2::read::GzDecoder;
use sps_common::error::{Result, SpsError};
use tar::{Archive, EntryType};
use tracing::{debug, error, warn};
use zip::ZipArchive;

#[cfg(target_os = "macos")]
use crate::utils::xattr;

pub(crate) fn infer_archive_root_dir(
    archive_path: &Path,
    archive_type: &str,
) -> Result<Option<PathBuf>> {
    tracing::debug!(
        "Inferring root directory for archive: {}",
        archive_path.display()
    );
    let file = File::open(archive_path).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed to open archive {}: {}", archive_path.display(), e),
        )))
    })?;

    match archive_type {
        "zip" => infer_zip_root(file, archive_path),
        "gz" | "tgz" => {
            let decompressed = GzDecoder::new(file);
            infer_tar_root(decompressed, archive_path)
        }
        "bz2" | "tbz" | "tbz2" => {
            let decompressed = BzDecoder::new(file);
            infer_tar_root(decompressed, archive_path)
        }
        "xz" | "txz" => {
            // Use external xz command to decompress, then read as tar
            infer_xz_tar_root(archive_path)
        }
        "tar" => infer_tar_root(file, archive_path),
        _ => Err(SpsError::Generic(format!(
            "Cannot infer root dir for unsupported archive type '{}' in {}",
            archive_type,
            archive_path.display()
        ))),
    }
}

fn infer_tar_root<R: Read>(reader: R, archive_path_for_log: &Path) -> Result<Option<PathBuf>> {
    let mut archive = Archive::new(reader);
    let mut unique_roots = HashSet::new();
    let mut non_empty_entry_found = false;
    let mut first_component_name: Option<PathBuf> = None;

    for entry_result in archive.entries()? {
        let entry = entry_result.map_err(|e| {
            SpsError::Generic(format!(
                "Error reading TAR entry from {}: {}",
                archive_path_for_log.display(),
                e
            ))
        })?;
        let path = entry
            .path()
            .map_err(|e| {
                SpsError::Generic(format!(
                    "Invalid path in TAR entry from {}: {}",
                    archive_path_for_log.display(),
                    e
                ))
            })?
            .into_owned();

        if path.components().next().is_none() {
            continue;
        }

        if let Some(first_comp) = path.components().next() {
            if let Component::Normal(name) = first_comp {
                non_empty_entry_found = true;
                let current_root = PathBuf::from(name);
                if first_component_name.is_none() {
                    first_component_name = Some(current_root.clone());
                }
                unique_roots.insert(current_root);

                if unique_roots.len() > 1 {
                    tracing::debug!(
                        "Multiple top-level items found in TAR {}, cannot infer single root.",
                        archive_path_for_log.display()
                    );
                    return Ok(None);
                }
            } else {
                tracing::debug!(
                    "Non-standard top-level component ({:?}) found in TAR {}, cannot infer single root.",
                    first_comp,
                    archive_path_for_log.display()
                );
                return Ok(None);
            }
        } else {
            tracing::debug!(
                "Empty or unusual path found in TAR {}, cannot infer single root.",
                archive_path_for_log.display()
            );
            return Ok(None);
        }
    }

    if unique_roots.len() == 1 && non_empty_entry_found {
        let inferred_root = first_component_name.unwrap();
        tracing::debug!(
            "Inferred single root directory in TAR {}: {}",
            archive_path_for_log.display(),
            inferred_root.display()
        );
        Ok(Some(inferred_root))
    } else if !non_empty_entry_found {
        tracing::warn!(
            "TAR archive {} appears to be empty or contain only metadata.",
            archive_path_for_log.display()
        );
        Ok(None)
    } else {
        tracing::debug!(
            "No single common root directory found in TAR {}. unique_roots count: {}",
            archive_path_for_log.display(),
            unique_roots.len()
        );
        Ok(None)
    }
}

fn infer_xz_tar_root(archive_path: &Path) -> Result<Option<PathBuf>> {
    // Create a temporary file for decompressed content
    let temp_file =
        tempfile::NamedTempFile::new().map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

    // Use external xz command to decompress
    let output = Command::new("xz")
        .args(["-dc", archive_path.to_str().unwrap()])
        .output()
        .map_err(|e| {
            SpsError::Generic(format!(
                "Failed to run xz command for decompression: {e}. Make sure xz is installed."
            ))
        })?;

    if !output.status.success() {
        return Err(SpsError::Generic(format!(
            "xz decompression failed: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Write decompressed data to temp file
    std::fs::write(temp_file.path(), &output.stdout)
        .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

    // Read as tar
    let file = File::open(temp_file.path()).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
    infer_tar_root(file, archive_path)
}

fn infer_zip_root<R: Read + Seek>(reader: R, archive_path: &Path) -> Result<Option<PathBuf>> {
    let mut archive = ZipArchive::new(reader).map_err(|e| {
        SpsError::Generic(format!(
            "Failed to open ZIP archive {}: {}",
            archive_path.display(),
            e
        ))
    })?;

    let mut root_candidates = HashSet::new();

    for i in 0..archive.len() {
        let file = archive
            .by_index(i)
            .map_err(|e| SpsError::Generic(format!("Failed to read ZIP entry {i}: {e}")))?;

        if let Some(enclosed_name) = file.enclosed_name() {
            if let Some(Component::Normal(name)) = enclosed_name.components().next() {
                root_candidates.insert(name.to_string_lossy().to_string());
            }
        }
    }

    if root_candidates.len() == 1 {
        let root = root_candidates.into_iter().next().unwrap();
        Ok(Some(PathBuf::from(root)))
    } else {
        Ok(None)
    }
}

#[cfg(target_os = "macos")]
pub fn quarantine_extracted_apps_in_stage(stage_dir: &Path, agent_name: &str) -> Result<()> {
    use std::fs;

    use tracing::{debug, warn};
    debug!(
        "Searching for .app bundles in {} to apply quarantine.",
        stage_dir.display()
    );
    if stage_dir.is_dir() {
        for entry_result in fs::read_dir(stage_dir)? {
            let entry = entry_result?;
            let entry_path = entry.path();
            if entry_path.is_dir() && entry_path.extension().is_some_and(|ext| ext == "app") {
                debug!(
                    "Found app bundle in stage: {}. Applying quarantine.",
                    entry_path.display()
                );
                if let Err(e) = xattr::set_quarantine_attribute(&entry_path, agent_name) {
                    warn!(
                        "Failed to set quarantine attribute on staged app {}: {}. Installation will continue.",
                        entry_path.display(),
                        e
                    );
                }
            }
        }
    }
    Ok(())
}

pub fn extract_archive(
    archive_path: &Path,
    target_dir: &Path,
    strip_components: usize,
    archive_type: &str,
) -> Result<()> {
    debug!(
        "Extracting archive '{}' (type: {}) to '{}' (strip_components={}) using native Rust crates.",
        archive_path.display(),
        archive_type,
        target_dir.display(),
        strip_components
    );

    fs::create_dir_all(target_dir).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!(
                "Failed to create target directory {}: {}",
                target_dir.display(),
                e
            ),
        )))
    })?;

    let file = File::open(archive_path).map_err(|e| {
        SpsError::Io(std::sync::Arc::new(std::io::Error::new(
            e.kind(),
            format!("Failed to open archive {}: {}", archive_path.display(), e),
        )))
    })?;

    let result = match archive_type {
        "zip" => extract_zip_archive(file, target_dir, strip_components, archive_path),
        "gz" | "tgz" => {
            let tar = GzDecoder::new(file);
            extract_tar_archive(tar, target_dir, strip_components, archive_path)
        }
        "bz2" | "tbz" | "tbz2" => {
            let tar = BzDecoder::new(file);
            extract_tar_archive(tar, target_dir, strip_components, archive_path)
        }
        "xz" | "txz" => extract_xz_tar_archive(archive_path, target_dir, strip_components),
        "tar" => extract_tar_archive(file, target_dir, strip_components, archive_path),
        _ => Err(SpsError::Generic(format!(
            "Unsupported archive type provided for extraction: '{}' for file {}",
            archive_type,
            archive_path.display()
        ))),
    };
    #[cfg(target_os = "macos")]
    {
        if result.is_ok() {
            // Only quarantine if main extraction was successful
            if let Err(e) = quarantine_extracted_apps_in_stage(target_dir, "sps-extractor") {
                tracing::warn!(
                    "Error during post-extraction quarantine scan for {}: {}",
                    archive_path.display(),
                    e
                );
            }
        }
    }
    result
}

/// Represents a hardlink operation that was deferred.
fn extract_xz_tar_archive(
    archive_path: &Path,
    target_dir: &Path,
    strip_components: usize,
) -> Result<()> {
    debug!(
        "Extracting XZ+TAR archive using external xz command: {}",
        archive_path.display()
    );

    // Create a temporary file for decompressed content
    let temp_file =
        tempfile::NamedTempFile::new().map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

    // Use external xz command to decompress
    let output = Command::new("xz")
        .args(["-dc", archive_path.to_str().unwrap()])
        .output()
        .map_err(|e| {
            SpsError::Generic(format!(
                "Failed to run xz command for extraction: {e}. Make sure xz is installed."
            ))
        })?;

    if !output.status.success() {
        return Err(SpsError::Generic(format!(
            "xz decompression failed during extraction: {}",
            String::from_utf8_lossy(&output.stderr)
        )));
    }

    // Write decompressed data to temp file
    std::fs::write(temp_file.path(), &output.stdout)
        .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;

    // Extract as tar
    let file = File::open(temp_file.path()).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
    extract_tar_archive(file, target_dir, strip_components, archive_path)
}

#[cfg(unix)]
struct DeferredHardLink {
    link_path_in_archive: PathBuf,
    target_name_in_archive: PathBuf,
}

fn extract_tar_archive<R: Read>(
    reader: R,
    target_dir: &Path,
    strip_components: usize,
    archive_path_for_log: &Path,
) -> Result<()> {
    let mut archive = Archive::new(reader);
    archive.set_preserve_permissions(true);
    archive.set_unpack_xattrs(true);
    archive.set_overwrite(true);

    debug!(
        "Starting TAR extraction for {}",
        archive_path_for_log.display()
    );

    #[cfg(unix)]
    let mut deferred_hardlinks: Vec<DeferredHardLink> = Vec::new();
    let mut errors: Vec<String> = Vec::new();

    for entry_result in archive.entries()? {
        let mut entry = entry_result.map_err(|e| {
            SpsError::Generic(format!(
                "Error reading TAR entry from {}: {}",
                archive_path_for_log.display(),
                e
            ))
        })?;

        let original_path_in_archive: PathBuf = entry
            .path()
            .map_err(|e| {
                SpsError::Generic(format!(
                    "Invalid path in TAR entry from {}: {}",
                    archive_path_for_log.display(),
                    e
                ))
            })?
            .into_owned();

        let stripped_components_iter: Vec<Component<'_>> = original_path_in_archive
            .components()
            .skip(strip_components)
            .collect();

        if stripped_components_iter.is_empty() {
            debug!(
                "Skipping entry due to strip_components: {:?}",
                original_path_in_archive
            );
            continue;
        }

        let mut final_target_path_on_disk = target_dir.to_path_buf();
        for comp in stripped_components_iter {
            match comp {
                Component::Normal(p) => final_target_path_on_disk.push(p),
                Component::CurDir => {}
                Component::ParentDir => {
                    let msg = format!(
                        "Unsafe '..' in TAR path {} after stripping in {}",
                        original_path_in_archive.display(),
                        archive_path_for_log.display()
                    );
                    error!("{}", msg);
                    errors.push(msg);
                    continue;
                }
                Component::Prefix(_) | Component::RootDir => {
                    let msg = format!(
                        "Disallowed component {:?} in TAR path {}",
                        comp,
                        original_path_in_archive.display()
                    );
                    error!("{}", msg);
                    errors.push(msg);
                    continue;
                }
            }
        }

        if !final_target_path_on_disk.starts_with(target_dir) {
            let msg = format!(
                "Path traversal {} -> {} detected in {}",
                original_path_in_archive.display(),
                final_target_path_on_disk.display(),
                archive_path_for_log.display()
            );
            error!("{}", msg);
            errors.push(msg);
            continue;
        }

        if let Some(parent) = final_target_path_on_disk.parent() {
            if !parent.exists() {
                fs::create_dir_all(parent).map_err(|e| {
                    SpsError::Io(std::sync::Arc::new(io::Error::new(
                        e.kind(),
                        format!("Failed create parent dir {}: {}", parent.display(), e),
                    )))
                })?;
            }
        }

        #[cfg(unix)]
        if entry.header().entry_type() == EntryType::Link {
            if let Ok(Some(link_name_in_archive)) = entry.link_name() {
                let deferred_link = DeferredHardLink {
                    link_path_in_archive: original_path_in_archive.clone(),
                    target_name_in_archive: link_name_in_archive.into_owned(),
                };
                debug!(
                    "Deferring hardlink: archive path '{}' -> archive target '{}'",
                    original_path_in_archive.display(),
                    deferred_link.target_name_in_archive.display()
                );
                deferred_hardlinks.push(deferred_link);
                continue;
            } else {
                let msg = format!(
                    "Hardlink entry '{}' in {} has no link target name.",
                    original_path_in_archive.display(),
                    archive_path_for_log.display()
                );
                warn!("{}", msg);
                errors.push(msg);
                continue;
            }
        }

        match entry.unpack(&final_target_path_on_disk) {
            Ok(_) => debug!(
                "Unpacked TAR entry to: {}",
                final_target_path_on_disk.display()
            ),
            Err(e) => {
                if e.kind() != io::ErrorKind::AlreadyExists {
                    let msg = format!(
                        "Failed to unpack entry {:?} to {}: {}. Entry type: {:?}",
                        original_path_in_archive,
                        final_target_path_on_disk.display(),
                        e,
                        entry.header().entry_type()
                    );
                    error!("{}", msg);
                    errors.push(msg);
                } else {
                    debug!("Entry already exists at {}, skipping unpack (tar crate overwrite=true handles this).", final_target_path_on_disk.display());
                }
            }
        }
    }

    #[cfg(unix)]
    for deferred in deferred_hardlinks {
        let mut disk_link_path = target_dir.to_path_buf();
        for comp in deferred
            .link_path_in_archive
            .components()
            .skip(strip_components)
        {
            if let Component::Normal(p) = comp {
                disk_link_path.push(p);
            }
            // Other components should have been caught by safety checks above
        }

        let mut disk_target_path = target_dir.to_path_buf();
        // The link_name_in_archive is relative to the archive root *before* stripping.
        // We need to apply stripping to it as well to find its final disk location.
        for comp in deferred
            .target_name_in_archive
            .components()
            .skip(strip_components)
        {
            if let Component::Normal(p) = comp {
                disk_target_path.push(p);
            }
        }

        if !disk_target_path.starts_with(target_dir) || !disk_link_path.starts_with(target_dir) {
            let msg = format!("Skipping deferred hardlink due to path traversal attempt: link '{}' -> target '{}'", disk_link_path.display(), disk_target_path.display());
            error!("{}", msg);
            errors.push(msg);
            continue;
        }

        debug!(
            "Attempting deferred hardlink: disk link path '{}' -> disk target path '{}'",
            disk_link_path.display(),
            disk_target_path.display()
        );

        if disk_target_path.exists() {
            if let Some(parent) = disk_link_path.parent() {
                if !parent.exists() {
                    if let Err(e) = fs::create_dir_all(parent) {
                        let msg = format!(
                            "Failed to create parent directory for deferred hardlink {}: {}",
                            disk_link_path.display(),
                            e
                        );
                        error!("{}", msg);
                        errors.push(msg);
                        continue;
                    }
                }
            }

            if disk_link_path.symlink_metadata().is_ok() {
                // Check if something (file or symlink) exists at the link creation spot
                if let Err(e) = fs::remove_file(&disk_link_path) {
                    // Attempt to remove it
                    warn!("Could not remove existing file/symlink at hardlink destination {}: {}. Hardlink creation may fail.", disk_link_path.display(), e);
                }
            }

            if let Err(e) = fs::hard_link(&disk_target_path, &disk_link_path) {
                let msg = format!(
                    "Failed to create deferred hardlink '{}' -> '{}': {}. Target exists: {}",
                    disk_link_path.display(),
                    disk_target_path.display(),
                    e,
                    disk_target_path.exists()
                );
                error!("{}", msg);
                errors.push(msg);
            } else {
                debug!(
                    "Successfully created deferred hardlink: '{}' -> '{}'",
                    disk_link_path.display(),
                    disk_target_path.display()
                );
            }
        } else {
            let msg = format!(
                "Target '{}' for deferred hardlink '{}' does not exist. Hardlink not created.",
                disk_target_path.display(),
                disk_link_path.display()
            );
            error!("{}", msg);
            errors.push(msg);
        }
    }

    if !errors.is_empty() {
        return Err(SpsError::InstallError(format!(
            "Failed during TAR extraction for {} with {} error(s): {}",
            archive_path_for_log.display(),
            errors.len(),
            errors.join("; ")
        )));
    }

    debug!(
        "Finished TAR extraction for {}",
        archive_path_for_log.display()
    );
    #[cfg(target_os = "macos")]
    {
        if let Err(e) = quarantine_extracted_apps_in_stage(target_dir, "sps-tar-extractor") {
            tracing::warn!(
                "Error during post-tar extraction quarantine scan for {}: {}",
                archive_path_for_log.display(),
                e
            );
        }
    }
    Ok(())
}

fn extract_zip_archive<R: Read + Seek>(
    reader: R,
    target_dir: &Path,
    strip_components: usize,
    archive_path_for_log: &Path,
) -> Result<()> {
    let mut archive = ZipArchive::new(reader).map_err(|e| {
        SpsError::Generic(format!(
            "Failed to open ZIP {}: {}",
            archive_path_for_log.display(),
            e
        ))
    })?;

    debug!(
        "Starting ZIP extraction for {}",
        archive_path_for_log.display()
    );

    for i in 0..archive.len() {
        let mut file = archive
            .by_index(i)
            .map_err(|e| SpsError::Generic(format!("Failed to read ZIP entry {i}: {e}")))?;

        let Some(original_path_in_archive) = file.enclosed_name() else {
            debug!("Skipping unsafe ZIP entry (no enclosed name)");
            continue;
        };

        let stripped_components_iter: Vec<Component<'_>> = original_path_in_archive
            .components()
            .skip(strip_components)
            .collect();

        if stripped_components_iter.is_empty() {
            debug!(
                "Skipping ZIP entry {} due to strip_components",
                original_path_in_archive.display()
            );
            continue;
        }

        let mut final_target_path_on_disk = target_dir.to_path_buf();
        for comp in stripped_components_iter {
            match comp {
                Component::Normal(p) => final_target_path_on_disk.push(p),
                Component::CurDir => {}
                Component::ParentDir => {
                    error!(
                        "Unsafe '..' in ZIP path {} after strip_components",
                        original_path_in_archive.display()
                    );
                    return Err(SpsError::Generic(format!(
                        "Unsafe '..' component in ZIP path {}",
                        original_path_in_archive.display()
                    )));
                }
                Component::Prefix(_) | Component::RootDir => {
                    error!(
                        "Disallowed component {:?} in ZIP path {}",
                        comp,
                        original_path_in_archive.display()
                    );
                    return Err(SpsError::Generic(format!(
                        "Disallowed component in ZIP path {}",
                        original_path_in_archive.display()
                    )));
                }
            }
        }

        if !final_target_path_on_disk.starts_with(target_dir) {
            error!(
                "ZIP path traversal detected: {} -> {}",
                original_path_in_archive.display(),
                final_target_path_on_disk.display()
            );
            return Err(SpsError::Generic(format!(
                "ZIP path traversal detected in {}",
                archive_path_for_log.display()
            )));
        }

        if let Some(parent) = final_target_path_on_disk.parent() {
            if !parent.exists() {
                fs::create_dir_all(parent).map_err(|e| {
                    SpsError::Io(std::sync::Arc::new(io::Error::new(
                        e.kind(),
                        format!(
                            "Failed to create parent directory {}: {}",
                            parent.display(),
                            e
                        ),
                    )))
                })?;
            }
        }

        if file.is_dir() {
            debug!(
                "Creating directory: {}",
                final_target_path_on_disk.display()
            );
            fs::create_dir_all(&final_target_path_on_disk).map_err(|e| {
                SpsError::Io(std::sync::Arc::new(io::Error::new(
                    e.kind(),
                    format!(
                        "Failed to create directory {}: {}",
                        final_target_path_on_disk.display(),
                        e
                    ),
                )))
            })?;
        } else {
            // Regular file
            if final_target_path_on_disk.exists() {
                match fs::remove_file(&final_target_path_on_disk) {
                    Ok(_) => {}
                    Err(e) if e.kind() == io::ErrorKind::NotFound => {}
                    Err(e) => return Err(SpsError::Io(std::sync::Arc::new(e))),
                }
            }

            debug!("Extracting file: {}", final_target_path_on_disk.display());
            let mut outfile = File::create(&final_target_path_on_disk).map_err(|e| {
                SpsError::Io(std::sync::Arc::new(io::Error::new(
                    e.kind(),
                    format!(
                        "Failed to create file {}: {}",
                        final_target_path_on_disk.display(),
                        e
                    ),
                )))
            })?;

            std::io::copy(&mut file, &mut outfile).map_err(|e| {
                SpsError::Io(std::sync::Arc::new(io::Error::new(
                    e.kind(),
                    format!(
                        "Failed to write file {}: {}",
                        final_target_path_on_disk.display(),
                        e
                    ),
                )))
            })?;

            // Set permissions on Unix systems
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                if let Some(mode) = file.unix_mode() {
                    let perms = std::fs::Permissions::from_mode(mode);
                    std::fs::set_permissions(&final_target_path_on_disk, perms)
                        .map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
                }
            }
        }
    }

    debug!(
        "Finished ZIP extraction for {}",
        archive_path_for_log.display()
    );

    // Apply quarantine to extracted apps on macOS
    #[cfg(target_os = "macos")]
    {
        if let Err(e) = quarantine_extracted_apps_in_stage(target_dir, "sps-zip-extractor") {
            tracing::warn!(
                "Error during post-zip extraction quarantine scan for {}: {}",
                archive_path_for_log.display(),
                e
            );
        }
    }

    Ok(())
}

// sps-core/src/model/formula.rs
// *** Corrected: Removed derive Deserialize from ResourceSpec, removed unused SpsError import,
// added ResourceSpec struct and parsing ***

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use semver::Version;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::Value;
use tracing::{debug, error};

use crate::dependency::{Dependency, DependencyTag, Requirement};
use crate::error::Result; // <-- Import only Result // Use log crate imports

// --- Resource Spec Struct ---
// *** Added struct definition, REMOVED #[derive(Deserialize)] ***
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ResourceSpec {
    pub name: String,
    pub url: String,
    pub sha256: String,
    // Add other potential fields like version if needed later
}

// --- Bottle Related Structs (Original structure) ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BottleFileSpec {
    pub url: String,
    pub sha256: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct BottleSpec {
    pub stable: Option<BottleStableSpec>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct BottleStableSpec {
    pub rebuild: u32,
    #[serde(default)]
    pub files: HashMap<String, BottleFileSpec>,
}

// --- Formula Version Struct (Original structure) ---
#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct FormulaVersions {
    pub stable: Option<String>,
    pub head: Option<String>,
    #[serde(default)]
    pub bottle: bool,
}

// --- Main Formula Struct ---
// *** Added 'resources' field ***
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Formula {
    pub name: String,
    pub stable_version_str: String,
    #[serde(rename = "versions")]
    pub version_semver: Version,
    #[serde(default)]
    pub revision: u32,
    #[serde(default)]
    pub desc: Option<String>,
    #[serde(default)]
    pub homepage: Option<String>,
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub sha256: String,
    #[serde(default)]
    pub mirrors: Vec<String>,
    #[serde(default)]
    pub bottle: BottleSpec,
    #[serde(skip_deserializing)]
    pub dependencies: Vec<Dependency>,
    #[serde(default, deserialize_with = "deserialize_requirements")]
    pub requirements: Vec<Requirement>,
    #[serde(skip_deserializing)] // Skip direct deserialization for this field
    pub resources: Vec<ResourceSpec>, // Stores parsed resources
    #[serde(skip)]
    pub install_keg_path: Option<PathBuf>,
}

// Custom deserialization logic for Formula
impl<'de> Deserialize<'de> for Formula {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Temporary struct reflecting the JSON structure more closely
        // *** Added 'resources' field to capture raw JSON Value ***
        #[derive(Deserialize, Debug)]
        struct RawFormulaData {
            name: String,
            #[serde(default)]
            revision: u32,
            desc: Option<String>,
            homepage: Option<String>,
            versions: FormulaVersions,
            #[serde(default)]
            url: String,
            #[serde(default)]
            sha256: String,
            #[serde(default)]
            mirrors: Vec<String>,
            #[serde(default)]
            bottle: BottleSpec,
            #[serde(default)]
            dependencies: Vec<String>,
            #[serde(default)]
            build_dependencies: Vec<String>,
            #[serde(default)]
            test_dependencies: Vec<String>,
            #[serde(default)]
            recommended_dependencies: Vec<String>,
            #[serde(default)]
            optional_dependencies: Vec<String>,
            #[serde(default, deserialize_with = "deserialize_requirements")]
            requirements: Vec<Requirement>,
            #[serde(default)]
            resources: Vec<Value>, // Capture resources as generic Value first
            #[serde(default)]
            urls: Option<Value>,
        }

        let raw: RawFormulaData = RawFormulaData::deserialize(deserializer)?;

        // --- Version Parsing (Original logic) ---
        let stable_version_str = raw
            .versions
            .stable
            .clone()
            .ok_or_else(|| de::Error::missing_field("versions.stable"))?;
        let version_semver = match crate::model::version::Version::parse(&stable_version_str) {
            Ok(v) => v.into(),
            Err(_) => {
                let mut majors = 0u32;
                let mut minors = 0u32;
                let mut patches = 0u32;
                let mut part_count = 0;
                for (i, part) in stable_version_str.split('.').enumerate() {
                    let numeric_part = part
                        .chars()
                        .take_while(|c| c.is_ascii_digit())
                        .collect::<String>();
                    if numeric_part.is_empty() && i > 0 {
                        break;
                    }
                    if numeric_part.len() < part.len() && i > 0 {
                        if let Ok(num) = numeric_part.parse::<u32>() {
                            match i {
                                0 => majors = num,
                                1 => minors = num,
                                2 => patches = num,
                                _ => {}
                            }
                            part_count += 1;
                        }
                        break;
                    }
                    if let Ok(num) = numeric_part.parse::<u32>() {
                        match i {
                            0 => majors = num,
                            1 => minors = num,
                            2 => patches = num,
                            _ => {}
                        }
                        part_count += 1;
                    }
                    if i >= 2 {
                        break;
                    }
                }
                let version_str_padded = match part_count {
                    1 => format!("{majors}.0.0"),
                    2 => format!("{majors}.{minors}.0"),
                    _ => format!("{majors}.{minors}.{patches}"),
                };
                match Version::parse(&version_str_padded) {
                    Ok(v) => v,
                    Err(_) => {
                        error!(
                            "Warning: Could not parse version '{}' (sanitized to '{}') for formula '{}'. Using 0.0.0.",
                            stable_version_str, version_str_padded, raw.name
                        );
                        Version::new(0, 0, 0)
                    }
                }
            }
        };

        // --- URL/SHA256 Logic (Original logic) ---
        let mut final_url = raw.url;
        let mut final_sha256 = raw.sha256;
        if final_url.is_empty() {
            if let Some(Value::Object(urls_map)) = raw.urls {
                if let Some(Value::Object(stable_url_info)) = urls_map.get("stable") {
                    if let Some(Value::String(u)) = stable_url_info.get("url") {
                        final_url = u.clone();
                    }
                    if let Some(Value::String(s)) = stable_url_info
                        .get("checksum")
                        .or_else(|| stable_url_info.get("sha256"))
                    {
                        final_sha256 = s.clone();
                    }
                }
            }
        }
        if final_url.is_empty() && raw.versions.head.is_none() {
            debug!("Warning: Formula '{}' has no stable URL defined.", raw.name);
        }

        // --- Dependency Processing (Original logic) ---
        let mut combined_dependencies: Vec<Dependency> = Vec::new();
        let mut seen_deps: HashMap<String, DependencyTag> = HashMap::new();
        let mut process_list = |deps: &[String], tag: DependencyTag| {
            for name in deps {
                *seen_deps
                    .entry(name.clone())
                    .or_insert(DependencyTag::empty()) |= tag;
            }
        };
        process_list(&raw.dependencies, DependencyTag::RUNTIME);
        process_list(&raw.build_dependencies, DependencyTag::BUILD);
        process_list(&raw.test_dependencies, DependencyTag::TEST);
        process_list(
            &raw.recommended_dependencies,
            DependencyTag::RECOMMENDED | DependencyTag::RUNTIME,
        );
        process_list(
            &raw.optional_dependencies,
            DependencyTag::OPTIONAL | DependencyTag::RUNTIME,
        );
        for (name, tags) in seen_deps {
            combined_dependencies.push(Dependency::new_with_tags(name, tags));
        }

        // --- Resource Processing ---
        // *** Added parsing logic for the 'resources' field ***
        let mut combined_resources: Vec<ResourceSpec> = Vec::new();
        for res_val in raw.resources {
            // Homebrew API JSON format puts resource spec inside a keyed object
            // e.g., { "resource_name": { "url": "...", "sha256": "..." } }
            if let Value::Object(map) = res_val {
                // Assume only one key-value pair per object in the array
                if let Some((res_name, res_spec_val)) = map.into_iter().next() {
                    // Use the manual Deserialize impl for ResourceSpec
                    match ResourceSpec::deserialize(res_spec_val.clone()) {
                        // Use ::deserialize
                        Ok(mut res_spec) => {
                            // Inject the name from the key if missing
                            if res_spec.name.is_empty() {
                                res_spec.name = res_name;
                            } else if res_spec.name != res_name {
                                debug!(
                                    "Resource name mismatch in formula '{}': key '{}' vs spec '{}'. Using key.",
                                    raw.name, res_name, res_spec.name
                                );
                                res_spec.name = res_name; // Prefer key name
                            }
                            // Ensure required fields are present
                            if res_spec.url.is_empty() || res_spec.sha256.is_empty() {
                                debug!(
                                    "Resource '{}' for formula '{}' is missing URL or SHA256. Skipping.",
                                    res_spec.name, raw.name
                                );
                                continue;
                            }
                            debug!(
                                "Parsed resource '{}' for formula '{}'",
                                res_spec.name, raw.name
                            );
                            combined_resources.push(res_spec);
                        }
                        Err(e) => {
                            // Use display for the error which comes from serde::de::Error::custom
                            debug!(
                                "Failed to parse resource spec value for key '{}' in formula '{}': {}. Value: {:?}",
                                res_name, raw.name, e, res_spec_val
                            );
                        }
                    }
                } else {
                    debug!("Empty resource object found in formula '{}'.", raw.name);
                }
            } else {
                debug!(
                    "Unexpected format for resource entry in formula '{}': expected object, got {:?}",
                    raw.name, res_val
                );
            }
        }

        Ok(Self {
            name: raw.name,
            stable_version_str,
            version_semver,
            revision: raw.revision,
            desc: raw.desc,
            homepage: raw.homepage,
            url: final_url,
            sha256: final_sha256,
            mirrors: raw.mirrors,
            bottle: raw.bottle,
            dependencies: combined_dependencies,
            requirements: raw.requirements,
            resources: combined_resources, // Assign parsed resources
            install_keg_path: None,
        })
    }
}

// --- Formula impl Methods ---
impl Formula {
    // dependencies() and requirements() are unchanged
    pub fn dependencies(&self) -> Result<Vec<Dependency>> {
        Ok(self.dependencies.clone())
    }
    pub fn requirements(&self) -> Result<Vec<Requirement>> {
        Ok(self.requirements.clone())
    }

    // *** Added: Returns a clone of the defined resources. ***
    pub fn resources(&self) -> Result<Vec<ResourceSpec>> {
        Ok(self.resources.clone())
    }

    // Other methods (set_keg_path, version_str_full, accessors) are unchanged
    pub fn set_keg_path(&mut self, path: PathBuf) {
        self.install_keg_path = Some(path);
    }
    pub fn version_str_full(&self) -> String {
        if self.revision > 0 {
            format!("{}_{}", self.stable_version_str, self.revision)
        } else {
            self.stable_version_str.clone()
        }
    }
    pub fn name(&self) -> &str {
        &self.name
    }
    pub fn version(&self) -> &Version {
        &self.version_semver
    }
    pub fn source_url(&self) -> &str {
        &self.url
    }
    pub fn source_sha256(&self) -> &str {
        &self.sha256
    }
    pub fn get_bottle_spec(&self, bottle_tag: &str) -> Option<&BottleFileSpec> {
        self.bottle.stable.as_ref()?.files.get(bottle_tag)
    }
}

// --- BuildEnvironment Dependency Interface (Unchanged) ---
pub trait FormulaDependencies {
    fn name(&self) -> &str;
    fn install_prefix(&self, cellar_path: &Path) -> Result<PathBuf>;
    fn resolved_runtime_dependency_paths(&self) -> Result<Vec<PathBuf>>;
    fn resolved_build_dependency_paths(&self) -> Result<Vec<PathBuf>>;
    fn all_resolved_dependency_paths(&self) -> Result<Vec<PathBuf>>;
}
impl FormulaDependencies for Formula {
    fn name(&self) -> &str {
        &self.name
    }
    fn install_prefix(&self, cellar_path: &Path) -> Result<PathBuf> {
        let version_string = self.version_str_full();
        Ok(cellar_path.join(self.name()).join(version_string))
    }
    fn resolved_runtime_dependency_paths(&self) -> Result<Vec<PathBuf>> {
        Ok(Vec::new())
    }
    fn resolved_build_dependency_paths(&self) -> Result<Vec<PathBuf>> {
        Ok(Vec::new())
    }
    fn all_resolved_dependency_paths(&self) -> Result<Vec<PathBuf>> {
        Ok(Vec::new())
    }
}

// --- Deserialization Helpers ---
// deserialize_requirements remains unchanged
fn deserialize_requirements<'de, D>(
    deserializer: D,
) -> std::result::Result<Vec<Requirement>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    #[derive(Deserialize, Debug)]
    struct ReqWrapper {
        #[serde(default)]
        name: String,
        #[serde(default)]
        version: Option<String>,
        #[serde(default)]
        cask: Option<String>,
        #[serde(default)]
        download: Option<String>,
    }
    let raw_reqs: Vec<Value> = Deserialize::deserialize(deserializer)?;
    let mut requirements = Vec::new();
    for req_val in raw_reqs {
        if let Ok(req_obj) = serde_json::from_value::<ReqWrapper>(req_val.clone()) {
            match req_obj.name.as_str() {
                "macos" => {
                    requirements.push(Requirement::MacOS(
                        req_obj.version.unwrap_or_else(|| "any".to_string()),
                    ));
                }
                "xcode" => {
                    requirements.push(Requirement::Xcode(
                        req_obj.version.unwrap_or_else(|| "any".to_string()),
                    ));
                }
                "cask" => {
                    requirements.push(Requirement::Other(format!(
                        "Cask Requirement: {}",
                        req_obj.cask.unwrap_or_else(|| "?".to_string())
                    )));
                }
                "download" => {
                    requirements.push(Requirement::Other(format!(
                        "Download Requirement: {}",
                        req_obj.download.unwrap_or_else(|| "?".to_string())
                    )));
                }
                _ => requirements.push(Requirement::Other(format!(
                    "Unknown requirement type: {req_obj:?}"
                ))),
            }
        } else if let Value::String(req_str) = req_val {
            match req_str.as_str() {
                "macos" => requirements.push(Requirement::MacOS("latest".to_string())),
                "xcode" => requirements.push(Requirement::Xcode("latest".to_string())),
                _ => {
                    requirements.push(Requirement::Other(format!("Simple requirement: {req_str}")))
                }
            }
        } else {
            debug!("Warning: Could not parse requirement: {:?}", req_val);
            requirements.push(Requirement::Other(format!(
                "Unparsed requirement: {req_val:?}"
            )));
        }
    }
    Ok(requirements)
}

// Manual impl Deserialize for ResourceSpec (unchanged, this is needed)
impl<'de> Deserialize<'de> for ResourceSpec {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct Helper {
            #[serde(default)]
            name: String, // name is often the key, not in the value
            url: String,
            sha256: String,
        }
        let helper = Helper::deserialize(deserializer)?;
        // Note: The actual resource name comes from the key in the map during Formula
        // deserialization
        Ok(Self {
            name: helper.name,
            url: helper.url,
            sha256: helper.sha256,
        })
    }
}

// sps-common/src/dependency/resolver.rs
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tracing::{debug, error, warn};

use crate::dependency::{Dependency, DependencyTag};
use crate::error::{Result, SpsError};
use crate::formulary::Formulary;
use crate::keg::KegRegistry;
use crate::model::formula::Formula;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeInstallStrategy {
    BottlePreferred,
    SourceOnly,
    BottleOrFail,
}

#[derive(Debug, Clone, Default)]
pub struct PerTargetInstallPreferences {
    pub force_source_build_targets: HashSet<String>,
    pub force_bottle_only_targets: HashSet<String>,
}

pub struct ResolutionContext<'a> {
    pub formulary: &'a Formulary,
    pub keg_registry: &'a KegRegistry,
    pub sps_prefix: &'a Path,
    pub include_optional: bool,
    pub include_test: bool,
    pub skip_recommended: bool,
    pub initial_target_preferences: &'a PerTargetInstallPreferences,
    pub build_all_from_source: bool,
    pub cascade_source_preference_to_dependencies: bool,
    pub has_bottle_for_current_platform: fn(&Formula) -> bool,
    pub initial_target_actions: &'a HashMap<String, crate::pipeline::JobAction>,
}

#[derive(Debug, Clone)]
pub struct ResolvedDependency {
    pub formula: Arc<Formula>,
    pub keg_path: Option<PathBuf>,
    pub opt_path: Option<PathBuf>,
    pub status: ResolutionStatus,
    pub accumulated_tags: DependencyTag,
    pub determined_install_strategy: NodeInstallStrategy,
    pub failure_reason: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolutionStatus {
    Installed,
    Missing,
    Requested,
    SkippedOptional,
    NotFound,
    Failed,
}

#[derive(Debug, Clone, Default)]
pub struct ResolvedGraph {
    pub install_plan: Vec<ResolvedDependency>,
    pub build_dependency_opt_paths: Vec<PathBuf>,
    pub runtime_dependency_opt_paths: Vec<PathBuf>,
    pub resolution_details: HashMap<String, ResolvedDependency>,
}

// Added empty constructor
impl ResolvedGraph {
    pub fn empty() -> Self {
        Default::default()
    }
}

pub struct DependencyResolver<'a> {
    context: ResolutionContext<'a>,
    formula_cache: HashMap<String, Arc<Formula>>,
    visiting: HashSet<String>,
    resolution_details: HashMap<String, ResolvedDependency>,
    errors: HashMap<String, Arc<SpsError>>,
}

impl<'a> DependencyResolver<'a> {
    pub fn new(context: ResolutionContext<'a>) -> Self {
        Self {
            context,
            formula_cache: HashMap::new(),
            visiting: HashSet::new(),
            resolution_details: HashMap::new(),
            errors: HashMap::new(),
        }
    }

    fn determine_node_install_strategy(
        &self,
        formula_name: &str,
        formula_arc: &Arc<Formula>,
        is_initial_target: bool,
        requesting_parent_strategy: Option<NodeInstallStrategy>,
    ) -> NodeInstallStrategy {
        if is_initial_target {
            if self
                .context
                .initial_target_preferences
                .force_source_build_targets
                .contains(formula_name)
            {
                return NodeInstallStrategy::SourceOnly;
            }
            if self
                .context
                .initial_target_preferences
                .force_bottle_only_targets
                .contains(formula_name)
            {
                return NodeInstallStrategy::BottleOrFail;
            }
        }

        if self.context.build_all_from_source {
            return NodeInstallStrategy::SourceOnly;
        }

        if self.context.cascade_source_preference_to_dependencies
            && matches!(
                requesting_parent_strategy,
                Some(NodeInstallStrategy::SourceOnly)
            )
        {
            return NodeInstallStrategy::SourceOnly;
        }
        if matches!(
            requesting_parent_strategy,
            Some(NodeInstallStrategy::BottleOrFail)
        ) {
            return NodeInstallStrategy::BottleOrFail;
        }

        let strategy = if (self.context.has_bottle_for_current_platform)(formula_arc) {
            NodeInstallStrategy::BottlePreferred
        } else {
            NodeInstallStrategy::SourceOnly
        };

        debug!(
            "Install strategy for '{formula_name}': {:?} (initial_target={is_initial_target}, parent={:?}, bottle_available={})",
            strategy,
            requesting_parent_strategy,
            (self.context.has_bottle_for_current_platform)(formula_arc)
        );
        strategy
    }

    pub fn resolve_targets(&mut self, targets: &[String]) -> Result<ResolvedGraph> {
        debug!("Starting dependency resolution for targets: {:?}", targets);
        self.visiting.clear();
        self.resolution_details.clear();
        self.errors.clear();

        for target_name in targets {
            if let Err(e) = self.resolve_recursive(target_name, DependencyTag::RUNTIME, true, None)
            {
                self.errors.insert(target_name.clone(), Arc::new(e));
                warn!(
                    "Resolution failed for target '{}', but continuing for others.",
                    target_name
                );
            }
        }

        debug!(
            "Raw resolved map after initial pass: {:?}",
            self.resolution_details
                .iter()
                .map(|(k, v)| (k.clone(), v.status, v.accumulated_tags))
                .collect::<Vec<_>>()
        );

        let sorted_list = match self.topological_sort() {
            Ok(list) => list,
            Err(e @ SpsError::DependencyError(_)) => {
                error!("Topological sort failed due to dependency cycle: {}", e);
                return Err(e);
            }
            Err(e) => {
                error!("Topological sort failed: {}", e);
                return Err(e);
            }
        };

        let install_plan: Vec<ResolvedDependency> = sorted_list
            .into_iter()
            .filter(|dep| {
                matches!(
                    dep.status,
                    ResolutionStatus::Missing | ResolutionStatus::Requested
                )
            })
            .collect();

        let mut build_paths = Vec::new();
        let mut runtime_paths = Vec::new();
        let mut seen_build_paths = HashSet::new();
        let mut seen_runtime_paths = HashSet::new();

        for dep in self.resolution_details.values() {
            if matches!(
                dep.status,
                ResolutionStatus::Installed
                    | ResolutionStatus::Requested
                    | ResolutionStatus::Missing
            ) {
                if let Some(opt_path) = &dep.opt_path {
                    if dep.accumulated_tags.contains(DependencyTag::BUILD)
                        && seen_build_paths.insert(opt_path.clone())
                    {
                        debug!("Adding build dep path: {}", opt_path.display());
                        build_paths.push(opt_path.clone());
                    }
                    if dep.accumulated_tags.intersects(
                        DependencyTag::RUNTIME
                            | DependencyTag::RECOMMENDED
                            | DependencyTag::OPTIONAL,
                    ) && seen_runtime_paths.insert(opt_path.clone())
                    {
                        debug!("Adding runtime dep path: {}", opt_path.display());
                        runtime_paths.push(opt_path.clone());
                    }
                } else if dep.status != ResolutionStatus::NotFound
                    && dep.status != ResolutionStatus::Failed
                {
                    debug!(
                        "Warning: No opt_path found for resolved dependency {} ({:?})",
                        dep.formula.name(),
                        dep.status
                    );
                }
            }
        }

        if !self.errors.is_empty() {
            warn!(
                "Resolution encountered errors for specific targets: {:?}",
                self.errors
                    .iter()
                    .map(|(k, v)| (k, v.to_string()))
                    .collect::<HashMap<_, _>>()
            );
        }

        debug!(
            "Final installation plan (needs install/build): {:?}",
            install_plan
                .iter()
                .map(|d| (d.formula.name(), d.status))
                .collect::<Vec<_>>()
        );
        debug!(
            "Collected build dependency paths: {:?}",
            build_paths.iter().map(|p| p.display()).collect::<Vec<_>>()
        );
        debug!(
            "Collected runtime dependency paths: {:?}",
            runtime_paths
                .iter()
                .map(|p| p.display())
                .collect::<Vec<_>>()
        );

        Ok(ResolvedGraph {
            install_plan,
            build_dependency_opt_paths: build_paths,
            runtime_dependency_opt_paths: runtime_paths,
            resolution_details: self.resolution_details.clone(),
        })
    }

    fn update_existing_resolution(
        &mut self,
        name: &str,
        tags_from_parent_edge: DependencyTag,
        is_initial_target: bool,
    ) -> Result<bool> {
        let Some(existing) = self.resolution_details.get_mut(name) else {
            return Ok(false);
        };

        let original_status = existing.status;
        let original_tags = existing.accumulated_tags;
        let has_keg = existing.keg_path.is_some();

        let mut new_status = original_status;
        if is_initial_target && new_status == ResolutionStatus::Missing {
            new_status = ResolutionStatus::Requested;
        }

        let skip_recommended = self.context.skip_recommended;
        let include_optional = self.context.include_optional;

        if Self::should_upgrade_optional_status_static(
            new_status,
            tags_from_parent_edge,
            is_initial_target,
            has_keg,
            skip_recommended,
            include_optional,
        ) {
            new_status = if has_keg {
                ResolutionStatus::Installed
            } else if is_initial_target {
                ResolutionStatus::Requested
            } else {
                ResolutionStatus::Missing
            };
        }

        let mut needs_revisit = false;
        if new_status != original_status {
            debug!(
                "Updating status for '{name}' from {:?} to {:?}",
                original_status, new_status
            );
            existing.status = new_status;
            needs_revisit = true;
        }

        let combined_tags = original_tags | tags_from_parent_edge;
        if combined_tags != original_tags {
            debug!(
                "Updating tags for '{name}' from {:?} to {:?}",
                original_tags, combined_tags
            );
            existing.accumulated_tags = combined_tags;
            needs_revisit = true;
        }

        if !needs_revisit {
            debug!("'{}' already resolved with compatible status/tags.", name);
        } else {
            debug!(
                "Re-evaluating dependencies for '{}' due to status/tag update",
                name
            );
        }

        Ok(needs_revisit)
    }

    fn should_upgrade_optional_status_static(
        current_status: ResolutionStatus,
        tags_from_parent_edge: DependencyTag,
        is_initial_target: bool,
        _has_keg: bool,
        skip_recommended: bool,
        include_optional: bool,
    ) -> bool {
        current_status == ResolutionStatus::SkippedOptional
            && (tags_from_parent_edge.contains(DependencyTag::RUNTIME)
                || tags_from_parent_edge.contains(DependencyTag::BUILD)
                || (tags_from_parent_edge.contains(DependencyTag::RECOMMENDED)
                    && !skip_recommended)
                || (is_initial_target && include_optional))
    }

    fn load_or_cache_formula(
        &mut self,
        name: &str,
        tags_from_parent_edge: DependencyTag,
    ) -> Result<Option<Arc<Formula>>> {
        if let Some(f) = self.formula_cache.get(name) {
            return Ok(Some(f.clone()));
        }

        debug!("Loading formula definition for '{}'", name);
        match self.context.formulary.load_formula(name) {
            Ok(f) => {
                let arc = Arc::new(f);
                self.formula_cache.insert(name.to_string(), arc.clone());
                Ok(Some(arc))
            }
            Err(e) => {
                error!("Failed to load formula definition for '{}': {}", name, e);
                let msg = e.to_string();
                self.resolution_details.insert(
                    name.to_string(),
                    ResolvedDependency {
                        formula: Arc::new(Formula::placeholder(name)),
                        keg_path: None,
                        opt_path: None,
                        status: ResolutionStatus::NotFound,
                        accumulated_tags: tags_from_parent_edge,
                        determined_install_strategy: NodeInstallStrategy::BottlePreferred,
                        failure_reason: Some(msg.clone()),
                    },
                );
                self.errors
                    .insert(name.to_string(), Arc::new(SpsError::NotFound(msg)));
                Ok(None)
            }
        }
    }

    fn create_initial_resolution(
        &mut self,
        name: &str,
        formula_arc: Arc<Formula>,
        tags_from_parent_edge: DependencyTag,
        is_initial_target: bool,
        requesting_parent_strategy: Option<NodeInstallStrategy>,
    ) -> Result<()> {
        let current_node_strategy = self.determine_node_install_strategy(
            name,
            &formula_arc,
            is_initial_target,
            requesting_parent_strategy,
        );

        let (status, keg_path) =
            self.determine_resolution_status(name, is_initial_target, current_node_strategy)?;

        debug!(
            "Initial status for '{}': {:?}, keg: {:?}, opt: {}",
            name,
            status,
            keg_path,
            self.context.keg_registry.get_opt_path(name).display()
        );

        self.resolution_details.insert(
            name.to_string(),
            ResolvedDependency {
                formula: formula_arc.clone(),
                keg_path,
                opt_path: Some(self.context.keg_registry.get_opt_path(name)),
                status,
                accumulated_tags: tags_from_parent_edge,
                determined_install_strategy: current_node_strategy,
                failure_reason: None,
            },
        );

        Ok(())
    }

    fn determine_resolution_status(
        &self,
        name: &str,
        is_initial_target: bool,
        strategy: NodeInstallStrategy,
    ) -> Result<(ResolutionStatus, Option<PathBuf>)> {
        match strategy {
            NodeInstallStrategy::SourceOnly => Ok((
                if is_initial_target {
                    ResolutionStatus::Requested
                } else {
                    ResolutionStatus::Missing
                },
                None,
            )),
            NodeInstallStrategy::BottlePreferred | NodeInstallStrategy::BottleOrFail => {
                if let Some(keg) = self.context.keg_registry.get_installed_keg(name)? {
                    // Check if this is an upgrade target - if so, mark as Requested even if
                    // installed
                    let should_request_upgrade = is_initial_target
                        && self
                            .context
                            .initial_target_actions
                            .get(name)
                            .map(|action| {
                                matches!(action, crate::pipeline::JobAction::Upgrade { .. })
                            })
                            .unwrap_or(false);

                    debug!("[Resolver] Package '{}': is_initial_target={}, should_request_upgrade={}, action={:?}",
                        name, is_initial_target, should_request_upgrade,
                        self.context.initial_target_actions.get(name));

                    if should_request_upgrade {
                        debug!(
                            "[Resolver] Marking upgrade target '{}' as Requested (was installed)",
                            name
                        );
                        Ok((ResolutionStatus::Requested, Some(keg.path)))
                    } else {
                        debug!("[Resolver] Marking '{}' as Installed (normal case)", name);
                        Ok((ResolutionStatus::Installed, Some(keg.path)))
                    }
                } else {
                    debug!(
                        "[Resolver] Package '{}' not installed, marking as {}",
                        name,
                        if is_initial_target {
                            "Requested"
                        } else {
                            "Missing"
                        }
                    );
                    Ok((
                        if is_initial_target {
                            ResolutionStatus::Requested
                        } else {
                            ResolutionStatus::Missing
                        },
                        None,
                    ))
                }
            }
        }
    }

    fn process_dependencies(
        &mut self,
        dep_snapshot: &ResolvedDependency,
        parent_name: &str,
    ) -> Result<()> {
        for dep in dep_snapshot.formula.dependencies()? {
            let dep_name = &dep.name;
            let dep_tags = dep.tags;
            let parent_formula_name = dep_snapshot.formula.name();
            let parent_strategy = dep_snapshot.determined_install_strategy;

            debug!(
                "RESOLVER: Evaluating edge: parent='{}' ({:?}), child='{}' ({:?})",
                parent_formula_name, parent_strategy, dep_name, dep_tags
            );

            if !self.should_consider_dependency(&dep) {
                if !self.resolution_details.contains_key(dep_name.as_str()) {
                    debug!("RESOLVER: Child '{}' of '{}' globally SKIPPED (e.g. optional/test not included). Tags: {:?}", dep_name, parent_formula_name, dep_tags);
                }
                continue;
            }

            let should_process = self.context.should_process_dependency_edge(
                &dep_snapshot.formula,
                dep_tags,
                parent_strategy,
            );

            if !should_process {
                debug!(
                    "RESOLVER: Edge from '{}' (Strategy: {:?}) to child '{}' (Tags: {:?}) was SKIPPED by should_process_dependency_edge.",
                    parent_formula_name, parent_strategy, dep_name, dep_tags
                );
                continue;
            }

            debug!(
                "RESOLVER: Edge from '{}' (Strategy: {:?}) to child '{}' (Tags: {:?}) WILL BE PROCESSED. Recursing.",
                parent_formula_name, parent_strategy, dep_name, dep_tags
            );

            if let Err(e) = self.resolve_recursive(dep_name, dep_tags, false, Some(parent_strategy))
            {
                // Log the error but don't necessarily stop all resolution for this branch yet
                warn!(
                    "Error resolving child dependency '{}' for parent '{}': {}",
                    dep_name, parent_name, e
                );
                // Optionally, mark parent as failed if child error is critical
                // self.errors.insert(parent_name.to_string(), Arc::new(e)); // Storing error for
                // parent if needed
            }
        }
        Ok(())
    }

    fn resolve_recursive(
        &mut self,
        name: &str,
        tags_from_parent_edge: DependencyTag,
        is_initial_target: bool,
        requesting_parent_strategy: Option<NodeInstallStrategy>,
    ) -> Result<()> {
        debug!(
            "Resolving: {} (requested as {:?}, is_target: {})",
            name, tags_from_parent_edge, is_initial_target
        );

        if self.visiting.contains(name) {
            error!("Dependency cycle detected involving: {}", name);
            return Err(SpsError::DependencyError(format!(
                "Dependency cycle detected involving '{name}'"
            )));
        }

        if self.update_existing_resolution(name, tags_from_parent_edge, is_initial_target)? {
            // Already exists and was updated, no need to reprocess
            return Ok(());
        }

        if self.resolution_details.contains_key(name) {
            // Already exists but didn't need update
            return Ok(());
        }

        // New resolution needed
        self.visiting.insert(name.to_string());

        let formula_arc = match self.load_or_cache_formula(name, tags_from_parent_edge) {
            Ok(Some(formula)) => formula,
            Ok(None) => {
                self.visiting.remove(name);
                return Ok(()); // Already handled error case
            }
            Err(e) => {
                self.visiting.remove(name);
                return Err(e);
            }
        };

        self.create_initial_resolution(
            name,
            formula_arc,
            tags_from_parent_edge,
            is_initial_target,
            requesting_parent_strategy,
        )?;

        let dep_snapshot = self
            .resolution_details
            .get(name)
            .expect("just inserted")
            .clone();

        if matches!(
            dep_snapshot.status,
            ResolutionStatus::Failed | ResolutionStatus::NotFound
        ) {
            self.visiting.remove(name);
            return Ok(());
        }

        self.process_dependencies(&dep_snapshot, name)?;

        self.visiting.remove(name);
        debug!("Finished resolving '{}'", name);
        Ok(())
    }

    fn topological_sort(&self) -> Result<Vec<ResolvedDependency>> {
        let mut in_degree: HashMap<String, usize> = HashMap::new();
        let mut adj: HashMap<String, HashSet<String>> = HashMap::new();
        let mut sorted_list = Vec::new();
        let mut queue = VecDeque::new();

        let relevant_nodes_map: HashMap<String, &ResolvedDependency> = self
            .resolution_details
            .iter()
            .filter(|(_, dep)| {
                !matches!(
                    dep.status,
                    ResolutionStatus::NotFound | ResolutionStatus::Failed
                )
            })
            .map(|(k, v)| (k.clone(), v))
            .collect();

        for (parent_name, parent_rd) in &relevant_nodes_map {
            adj.entry(parent_name.clone()).or_default();
            in_degree.entry(parent_name.clone()).or_default();

            let parent_strategy = parent_rd.determined_install_strategy;
            if let Ok(dependencies) = parent_rd.formula.dependencies() {
                for child_edge in dependencies {
                    let child_name = &child_edge.name;
                    if relevant_nodes_map.contains_key(child_name)
                        && self.context.should_process_dependency_edge(
                            &parent_rd.formula,
                            child_edge.tags,
                            parent_strategy,
                        )
                        && adj
                            .entry(parent_name.clone())
                            .or_default()
                            .insert(child_name.clone())
                    {
                        *in_degree.entry(child_name.clone()).or_default() += 1;
                    }
                }
            }
        }

        for name in relevant_nodes_map.keys() {
            if *in_degree.get(name).unwrap_or(&1) == 0 {
                queue.push_back(name.clone());
            }
        }

        while let Some(u_name) = queue.pop_front() {
            if let Some(resolved_dep) = relevant_nodes_map.get(&u_name) {
                sorted_list.push((**resolved_dep).clone()); // Deref Arc then clone
                                                            // ResolvedDependency
            }
            if let Some(neighbors) = adj.get(&u_name) {
                for v_name in neighbors {
                    if relevant_nodes_map.contains_key(v_name) {
                        // Check if neighbor is relevant
                        if let Some(degree) = in_degree.get_mut(v_name) {
                            *degree = degree.saturating_sub(1);
                            if *degree == 0 {
                                queue.push_back(v_name.clone());
                            }
                        }
                    }
                }
            }
        }

        // Check for cycles: if sorted_list's length doesn't match relevant_nodes_map's length
        // (excluding already installed, skipped optional if not included, etc.)
        // A more direct check is if in_degree still contains non-zero values for relevant nodes.
        let mut cycle_detected = false;
        for (name, &degree) in &in_degree {
            if degree > 0 && relevant_nodes_map.contains_key(name) {
                // Further check if this node should have been processed (not skipped globally)
                if let Some(detail) = self.resolution_details.get(name) {
                    if self
                        .context
                        .should_consider_edge_globally(detail.accumulated_tags)
                    {
                        error!("Cycle detected or unresolved dependency: Node '{}' still has in-degree {}. Tags: {:?}", name, degree, detail.accumulated_tags);
                        cycle_detected = true;
                    } else {
                        debug!("Node '{}' has in-degree {} but was globally skipped. Tags: {:?}. Not a cycle error.", name, degree, detail.accumulated_tags);
                    }
                }
            }
        }

        if cycle_detected {
            return Err(SpsError::DependencyError(
                "Circular dependency detected or graph resolution incomplete".to_string(),
            ));
        }

        Ok(sorted_list) // Return the full sorted list of relevant nodes
    }

    fn should_consider_dependency(&self, dep: &Dependency) -> bool {
        let tags = dep.tags;
        if tags.contains(DependencyTag::TEST) && !self.context.include_test {
            return false;
        }
        if tags.contains(DependencyTag::OPTIONAL) && !self.context.include_optional {
            return false;
        }
        if tags.contains(DependencyTag::RECOMMENDED) && self.context.skip_recommended {
            return false;
        }
        true
    }
}

impl Formula {
    fn placeholder(name: &str) -> Self {
        Self {
            name: name.to_string(),
            stable_version_str: "0.0.0".to_string(),
            version_semver: semver::Version::new(0, 0, 0), // Direct use
            revision: 0,
            desc: Some("Placeholder for unresolved formula".to_string()),
            homepage: None,
            url: String::new(),
            sha256: String::new(),
            mirrors: Vec::new(),
            bottle: Default::default(),
            dependencies: Vec::new(),
            requirements: Vec::new(),
            resources: Vec::new(),
            install_keg_path: None,
        }
    }
}

impl<'a> ResolutionContext<'a> {
    pub fn should_process_dependency_edge(
        &self,
        parent_formula_for_logging: &Arc<Formula>,
        edge_tags: DependencyTag,
        parent_node_determined_strategy: NodeInstallStrategy,
    ) -> bool {
        if !self.should_consider_edge_globally(edge_tags) {
            debug!(
                "Edge with tags {:?} for child of '{}' globally SKIPPED (e.g. optional/test not included).",
                edge_tags, parent_formula_for_logging.name()
            );
            return false;
        }

        match parent_node_determined_strategy {
            NodeInstallStrategy::BottlePreferred | NodeInstallStrategy::BottleOrFail => {
                let is_purely_build_dependency = edge_tags.contains(DependencyTag::BUILD)
                    && !edge_tags.intersects(
                        DependencyTag::RUNTIME
                            | DependencyTag::RECOMMENDED
                            | DependencyTag::OPTIONAL,
                    );
                if is_purely_build_dependency {
                    debug!("Edge with tags {:?} SKIPPED: Pure BUILD dependency of a bottle-installed parent '{}'.", edge_tags, parent_formula_for_logging.name());
                    return false;
                }
            }
            NodeInstallStrategy::SourceOnly => {
                // Process all relevant (non-globally-skipped) dependencies for source builds
            }
        }
        debug!(
            "Edge with tags {:?} WILL BE PROCESSED for parent '{}' (strategy {:?}).",
            edge_tags,
            parent_formula_for_logging.name(),
            parent_node_determined_strategy
        );
        true
    }

    pub fn should_consider_edge_globally(&self, edge_tags: DependencyTag) -> bool {
        if edge_tags.contains(DependencyTag::TEST) && !self.include_test {
            return false;
        }
        if edge_tags.contains(DependencyTag::OPTIONAL) && !self.include_optional {
            return false;
        }
        if edge_tags.contains(DependencyTag::RECOMMENDED) && self.skip_recommended {
            return false;
        }
        true
    }
}

use std::fs;
use std::path::Path;

use sps_common::config::Config;
use tracing::debug;

/// Robustly removes a file or directory, handling symlinks and permissions.
/// If `use_sudo_if_needed` is true, will attempt `sudo rm -rf` on permission errors.
pub fn remove_path_robustly(path: &Path, _config: &Config, use_sudo_if_needed: bool) -> bool {
    if !path.exists() && path.symlink_metadata().is_err() {
        debug!("Path {} not found for removal.", path.display());
        return true;
    }
    let is_dir = path.is_dir()
        && !path
            .symlink_metadata()
            .is_ok_and(|m| m.file_type().is_symlink());
    let removal_op = || -> std::io::Result<()> {
        if is_dir {
            fs::remove_dir_all(path)
        } else {
            fs::remove_file(path)
        }
    };

    if let Err(e) = removal_op() {
        if e.kind() == std::io::ErrorKind::NotFound {
            return true;
        }
        if use_sudo_if_needed && e.kind() == std::io::ErrorKind::PermissionDenied {
            debug!(
                "Direct removal of {} failed (Permission Denied). Trying with sudo rm -rf.",
                path.display()
            );
            let output = std::process::Command::new("sudo")
                .arg("rm")
                .arg("-rf")
                .arg(path)
                .output();
            match output {
                Ok(out) if out.status.success() => {
                    debug!("Successfully removed {} with sudo.", path.display());
                    return true;
                }
                Ok(out) => {
                    debug!(
                        "`sudo rm -rf {}` failed ({}): {}",
                        path.display(),
                        out.status,
                        String::from_utf8_lossy(&out.stderr).trim()
                    );
                    return false;
                }
                Err(sudo_e) => {
                    debug!(
                        "Error executing `sudo rm -rf` for {}: {}",
                        path.display(),
                        sudo_e
                    );
                    return false;
                }
            }
        } else {
            debug!("Failed to remove {}: {}", path.display(), e);
            return false;
        }
    }
    debug!("Successfully removed {}.", path.display());
    true
}

/// Recursively cleans up empty parent directories in the private cask store.
/// Starts from the given path and walks up, removing empty directories until a non-empty or root is
/// found.
pub fn cleanup_empty_parent_dirs_in_private_store(start_path: &Path, stop_at: &Path) {
    let mut current = start_path.to_path_buf();
    while current != *stop_at {
        if let Ok(read_dir) = fs::read_dir(&current) {
            if read_dir.count() == 0 {
                match fs::remove_dir(&current) {
                    Ok(_) => {
                        debug!("Removed empty directory: {}", current.display());
                    }
                    Err(e) => {
                        debug!("Failed to remove directory {}: {}", current.display(), e);
                        break;
                    }
                }
            } else {
                break;
            }
        } else {
            break;
        }
        if let Some(parent) = current.parent() {
            current = parent.to_path_buf();
        } else {
            break;
        }
    }
}

//! Contains the logic for the `info` command.

use std::sync::Arc;

use clap::Args;
use colored::Colorize;
use serde_json::Value;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_net::api;

#[derive(Args, Debug)]
pub struct Info {
    /// Name of the formula or cask
    pub name: String,

    /// Show information for a cask, not a formula
    #[arg(long)]
    pub cask: bool,
}

impl Info {
    /// Displays detailed information about a formula or cask.
    pub async fn run(&self, _config: &Config, cache: Arc<Cache>) -> Result<()> {
        let name = &self.name;
        let is_cask = self.cask;
        tracing::debug!("Getting info for package: {name}, is_cask: {is_cask}",);

        // Print loading message instead of spinner
        println!("Loading info for {name}");

        if self.cask {
            match get_cask_info(Arc::clone(&cache), name).await {
                Ok(info) => {
                    print_cask_info(name, &info);
                    Ok(())
                }
                Err(e) => Err(e),
            }
        } else {
            match get_formula_info_raw(Arc::clone(&cache), name).await {
                Ok(info) => {
                    // Removed bottle check logic here as it was complex and potentially racy.
                    // We'll try formula first, then cask if formula fails.
                    print_formula_info(name, &info);
                    return Ok(());
                }
                Err(SpsError::NotFound(_)) | Err(SpsError::Generic(_)) => {
                    // If formula lookup failed (not found or generic error), try cask.
                    tracing::debug!("Formula '{}' info failed, trying cask.", name);
                }
                Err(e) => {
                    return Err(e); // Propagate other errors (API, JSON, etc.)
                }
            }
            // --- Cask Fallback ---
            match get_cask_info(Arc::clone(&cache), name).await {
                Ok(info) => {
                    print_cask_info(name, &info);
                    Ok(())
                }
                Err(e) => {
                    Err(e) // Return the cask error if both formula and cask fail
                }
            }
        }
    }
}

/// Retrieves formula information from the cache or API as raw JSON
async fn get_formula_info_raw(cache: Arc<Cache>, name: &str) -> Result<Value> {
    match cache.load_raw("formula.json") {
        Ok(formula_data) => {
            let formulas: Vec<Value> =
                serde_json::from_str(&formula_data).map_err(SpsError::from)?;
            for formula in formulas {
                if let Some(fname) = formula.get("name").and_then(Value::as_str) {
                    if fname == name {
                        return Ok(formula);
                    }
                }
                // Also check aliases if needed
                if let Some(aliases) = formula.get("aliases").and_then(|a| a.as_array()) {
                    if aliases.iter().any(|a| a.as_str() == Some(name)) {
                        return Ok(formula);
                    }
                }
            }
            tracing::debug!("Formula '{}' not found within cached 'formula.json'.", name);
            // Explicitly return NotFound if not in cache
            return Err(SpsError::NotFound(format!(
                "Formula '{name}' not found in cache"
            )));
        }
        Err(e) => tracing::debug!(
            "Cache file 'formula.json' not found or failed to load ({}). Fetching from API.",
            e
        ),
    }
    tracing::debug!("Fetching formula '{}' directly from API", name);
    // api::fetch_formula returns Value directly now
    let value = api::fetch_formula(name).await?;
    // Store in cache if fetched successfully
    // Note: This might overwrite the full list cache, consider storing individual files or a map
    // cache.store_raw(&format!("formula/{}.json", name), &value.to_string())?; // Example of
    // storing individually
    Ok(value)
}

/// Retrieves cask information from the cache or API
async fn get_cask_info(cache: Arc<Cache>, name: &str) -> Result<Value> {
    match cache.load_raw("cask.json") {
        Ok(cask_data) => {
            let casks: Vec<Value> = serde_json::from_str(&cask_data).map_err(SpsError::from)?;
            for cask in casks {
                if let Some(token) = cask.get("token").and_then(Value::as_str) {
                    if token == name {
                        return Ok(cask);
                    }
                }
                // Check aliases if needed
                if let Some(aliases) = cask.get("aliases").and_then(|a| a.as_array()) {
                    if aliases.iter().any(|a| a.as_str() == Some(name)) {
                        return Ok(cask);
                    }
                }
            }
            tracing::debug!("Cask '{}' not found within cached 'cask.json'.", name);
            // Explicitly return NotFound if not in cache
            return Err(SpsError::NotFound(format!(
                "Cask '{name}' not found in cache"
            )));
        }
        Err(e) => tracing::debug!(
            "Cache file 'cask.json' not found or failed to load ({}). Fetching from API.",
            e
        ),
    }
    tracing::debug!("Fetching cask '{}' directly from API", name);
    // api::fetch_cask returns Value directly now
    let value = api::fetch_cask(name).await?;
    // Store in cache if fetched successfully
    // cache.store_raw(&format!("cask/{}.json", name), &value.to_string())?; // Example of storing
    // individually
    Ok(value)
}

/// Prints formula information in a formatted table
fn print_formula_info(_name: &str, formula: &Value) {
    // Basic info extraction
    let full_name = formula
        .get("full_name")
        .and_then(|f| f.as_str())
        .unwrap_or("N/A");
    let version = formula
        .get("versions")
        .and_then(|v| v.get("stable"))
        .and_then(|s| s.as_str())
        .unwrap_or("N/A");
    let revision = formula
        .get("revision")
        .and_then(|r| r.as_u64())
        .unwrap_or(0);
    let version_str = if revision > 0 {
        format!("{version}_{revision}")
    } else {
        version.to_string()
    };
    let license = formula
        .get("license")
        .and_then(|l| l.as_str())
        .unwrap_or("N/A");
    let homepage = formula
        .get("homepage")
        .and_then(|h| h.as_str())
        .unwrap_or("N/A");

    // Header
    println!("{}", format!("Formula: {full_name}").green().bold());

    // Summary table
    let mut table = prettytable::Table::new();
    table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
    table.add_row(prettytable::row!["Version", version_str]);
    table.add_row(prettytable::row!["License", license]);
    table.add_row(prettytable::row!["Homepage", homepage]);
    table.printstd();

    // Detailed sections
    if let Some(desc) = formula.get("desc").and_then(|d| d.as_str()) {
        if !desc.is_empty() {
            println!("\n{}", "Description".blue().bold());
            println!("{desc}");
        }
    }
    if let Some(caveats) = formula.get("caveats").and_then(|c| c.as_str()) {
        if !caveats.is_empty() {
            println!("\n{}", "Caveats".blue().bold());
            println!("{caveats}");
        }
    }

    // Combined Dependencies Section
    let mut dep_table = prettytable::Table::new();
    dep_table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
    let mut has_deps = false;

    let mut add_deps = |title: &str, key: &str, tag: &str| {
        if let Some(deps) = formula.get(key).and_then(|d| d.as_array()) {
            let dep_list: Vec<&str> = deps.iter().filter_map(|d| d.as_str()).collect();
            if !dep_list.is_empty() {
                has_deps = true;
                for (i, d) in dep_list.iter().enumerate() {
                    let display_title = if i == 0 { title } else { "" };
                    let display_tag = if i == 0 {
                        format!("({tag})")
                    } else {
                        "".to_string()
                    };
                    dep_table.add_row(prettytable::row![display_title, d, display_tag]);
                }
            }
        }
    };

    add_deps("Required", "dependencies", "runtime");
    add_deps(
        "Recommended",
        "recommended_dependencies",
        "runtime, recommended",
    );
    add_deps("Optional", "optional_dependencies", "runtime, optional");
    add_deps("Build", "build_dependencies", "build");
    add_deps("Test", "test_dependencies", "test");

    if has_deps {
        println!("\n{}", "Dependencies".blue().bold());
        dep_table.printstd();
    }

    // Installation hint
    println!("\n{}", "Installation".blue().bold());
    println!(
        "  {} install {}",
        "sps".cyan(),
        formula
            .get("name")
            .and_then(|n| n.as_str())
            .unwrap_or(full_name) // Use short name if available
    );
}

/// Prints cask information in a formatted table
fn print_cask_info(name: &str, cask: &Value) {
    // Header
    println!("{}", format!("Cask: {name}").green().bold());

    // Summary table
    let mut table = prettytable::Table::new();
    table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
    if let Some(names) = cask.get("name").and_then(|n| n.as_array()) {
        if let Some(first) = names.first().and_then(|s| s.as_str()) {
            table.add_row(prettytable::row!["Name", first]);
        }
    }
    if let Some(desc) = cask.get("desc").and_then(|d| d.as_str()) {
        table.add_row(prettytable::row!["Description", desc]);
    }
    if let Some(homepage) = cask.get("homepage").and_then(|h| h.as_str()) {
        table.add_row(prettytable::row!["Homepage", homepage]);
    }
    if let Some(version) = cask.get("version").and_then(|v| v.as_str()) {
        table.add_row(prettytable::row!["Version", version]);
    }
    if let Some(url) = cask.get("url").and_then(|u| u.as_str()) {
        table.add_row(prettytable::row!["Download URL", url]);
    }
    // Add SHA if present
    if let Some(sha) = cask.get("sha256").and_then(|s| s.as_str()) {
        if !sha.is_empty() {
            table.add_row(prettytable::row!["SHA256", sha]);
        }
    }
    table.printstd();

    // Dependencies Section
    if let Some(deps) = cask.get("depends_on").and_then(|d| d.as_object()) {
        let mut dep_table = prettytable::Table::new();
        dep_table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        let mut has_deps = false;

        if let Some(formulas) = deps.get("formula").and_then(|f| f.as_array()) {
            if !formulas.is_empty() {
                has_deps = true;
                dep_table.add_row(prettytable::row![
                    "Formula".yellow(),
                    formulas
                        .iter()
                        .map(|v| v.as_str().unwrap_or(""))
                        .collect::<Vec<_>>()
                        .join(", ")
                ]);
            }
        }
        if let Some(casks) = deps.get("cask").and_then(|c| c.as_array()) {
            if !casks.is_empty() {
                has_deps = true;
                dep_table.add_row(prettytable::row![
                    "Cask".yellow(),
                    casks
                        .iter()
                        .map(|v| v.as_str().unwrap_or(""))
                        .collect::<Vec<_>>()
                        .join(", ")
                ]);
            }
        }
        if let Some(macos) = deps.get("macos") {
            has_deps = true;
            let macos_str = match macos {
                Value::String(s) => s.clone(),
                Value::Array(arr) => arr
                    .iter()
                    .map(|v| v.as_str().unwrap_or(""))
                    .collect::<Vec<_>>()
                    .join(" or "),
                _ => "Unknown".to_string(),
            };
            dep_table.add_row(prettytable::row!["macOS".yellow(), macos_str]);
        }

        if has_deps {
            println!("\n{}", "Dependencies".blue().bold());
            dep_table.printstd();
        }
    }

    // Installation hint
    println!("\n{}", "Installation".blue().bold());
    println!(
        "  {} install --cask {}", // Always use --cask for clarity
        "sps".cyan(),
        name // Use the token 'name' passed to the function
    );
}
// Removed is_bottle_available check

// sps/src/pipeline/downloader.rs
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

use reqwest::Client as HttpClient;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::model::InstallTargetIdentifier;
use sps_common::pipeline::{DownloadOutcome, PipelineEvent, PlannedJob};
use sps_common::SpsError;
use sps_core::{build, install};
use sps_net::http::ProgressCallback;
use sps_net::UrlField;
use tokio::sync::{broadcast, mpsc};
use tokio::task::JoinSet;
use tracing::{error, warn};

use super::runner::get_panic_message;

pub(crate) struct DownloadCoordinator {
    config: Config,
    cache: Arc<Cache>,
    http_client: Arc<HttpClient>,
    event_tx: Option<broadcast::Sender<PipelineEvent>>,
}

impl DownloadCoordinator {
    pub fn new(
        config: Config,
        cache: Arc<Cache>,
        http_client: Arc<HttpClient>,
        event_tx: broadcast::Sender<PipelineEvent>,
    ) -> Self {
        Self {
            config,
            cache,
            http_client,
            event_tx: Some(event_tx),
        }
    }

    pub async fn coordinate_downloads(
        &mut self,
        planned_jobs: Vec<PlannedJob>,
        download_outcome_tx: mpsc::Sender<DownloadOutcome>,
    ) -> Vec<(String, SpsError)> {
        let mut download_tasks = JoinSet::new();
        let mut critical_spawn_errors: Vec<(String, SpsError)> = Vec::new();

        for planned_job in planned_jobs {
            let _job_id_for_task = planned_job.target_id.clone();

            let task_config = self.config.clone();
            let task_cache = Arc::clone(&self.cache);
            let task_http_client = Arc::clone(&self.http_client);
            let task_event_tx = self.event_tx.as_ref().cloned();
            let outcome_tx_clone = download_outcome_tx.clone();
            let current_planned_job_for_task = planned_job.clone();

            download_tasks.spawn(async move {
                let job_id_in_task = current_planned_job_for_task.target_id.clone();
                let download_path_result: Result<PathBuf, SpsError>;

                if let Some(private_path) = current_planned_job_for_task.use_private_store_source.clone() {
                    download_path_result = Ok(private_path);
                } else {
                    let display_url_for_event = match &current_planned_job_for_task.target_definition {
                        InstallTargetIdentifier::Formula(f) => {
                            if !current_planned_job_for_task.is_source_build {
                                sps_core::install::bottle::exec::get_bottle_for_platform(f)
                                    .map_or_else(|_| f.url.clone(), |(_, spec)| spec.url.clone())
                            } else {
                                f.url.clone()
                            }
                        }
                        InstallTargetIdentifier::Cask(c) => match &c.url {
                            Some(UrlField::Simple(s)) => s.clone(),
                            Some(UrlField::WithSpec { url, .. }) => url.clone(),
                            None => "N/A (No Cask URL)".to_string(),
                        },
                    };

                    if display_url_for_event == "N/A (No Cask URL)"
                        || (display_url_for_event.is_empty() && !current_planned_job_for_task.is_source_build)
                    {
                        let _err_msg = "Download URL is missing or invalid".to_string();
                        let sps_err = SpsError::Generic(format!(
                            "Download URL is missing or invalid for job {job_id_in_task}"
                        ));
                        if let Some(ref tx) = task_event_tx {
                            tx.send(PipelineEvent::download_failed(
                                job_id_in_task.clone(),
                                display_url_for_event,
                                &sps_err,
                            )).ok();
                        }
                        download_path_result = Err(sps_err);
                    } else {
                        if let Some(ref tx) = task_event_tx {
                            tx.send(PipelineEvent::DownloadStarted {
                                target_id: job_id_in_task.clone(),
                                url: display_url_for_event.clone(),
                            }).ok();
                        }

                        // Create progress callback
                        let progress_callback: Option<ProgressCallback> = if let Some(ref tx) = task_event_tx {
                            let tx_clone = tx.clone();
                            let job_id_for_callback = job_id_in_task.clone();
                            Some(Arc::new(move |bytes_so_far: u64, total_size: Option<u64>| {
                                let _ = tx_clone.send(PipelineEvent::DownloadProgressUpdate {
                                    target_id: job_id_for_callback.clone(),
                                    bytes_so_far,
                                    total_size,
                                });
                            }))
                        } else {
                            None
                        };

                        let actual_download_result: Result<(PathBuf, bool), SpsError> =
                            match &current_planned_job_for_task.target_definition {
                                InstallTargetIdentifier::Formula(f) => {
                                    if current_planned_job_for_task.is_source_build {
                                        build::compile::download_source_with_progress(f, &task_config, progress_callback).await.map(|p| (p, false))
                                    } else {
                                        install::bottle::exec::download_bottle_with_progress_and_cache_info(
                                            f,
                                            &task_config,
                                            &task_http_client,
                                            progress_callback,
                                        )
                                        .await
                                    }
                                }
                                InstallTargetIdentifier::Cask(c) => {
                                    install::cask::download_cask_with_progress(c, task_cache.as_ref(), progress_callback).await.map(|p| (p, false))
                                }
                            };

                        match actual_download_result {
                            Ok((path, was_cached)) => {
                                let size_bytes = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
                                if let Some(ref tx) = task_event_tx {
                                    if was_cached {
                                        tx.send(PipelineEvent::DownloadCached {
                                            target_id: job_id_in_task.clone(),
                                            size_bytes,
                                        }).ok();
                                    } else {
                                        tx.send(PipelineEvent::DownloadFinished {
                                            target_id: job_id_in_task.clone(),
                                            path: path.clone(),
                                            size_bytes,
                                        }).ok();
                                    }
                                }
                                download_path_result = Ok(path);
                            }
                            Err(e) => {
                                warn!(
                                    "[DownloaderTask:{}] Download failed from {}: {}",
                                    job_id_in_task, display_url_for_event, e
                                );
                                if let Some(ref tx) = task_event_tx {
                                    tx.send(PipelineEvent::download_failed(
                                        job_id_in_task.clone(),
                                        display_url_for_event,
                                        &e,
                                    )).ok();
                                }
                                download_path_result = Err(e);
                            }
                        }
                    }
                }

                let outcome = DownloadOutcome {
                    planned_job: current_planned_job_for_task,
                    result: download_path_result,
                };

                if let Err(send_err) = outcome_tx_clone.send(outcome).await {
                    error!(
                        "[DownloaderTask:{}] CRITICAL: Failed to send download outcome to runner: {}. Job processing will likely stall.",
                        job_id_in_task, send_err
                    );
                }
            });
        }

        while let Some(join_result) = download_tasks.join_next().await {
            if let Err(e) = join_result {
                let panic_msg = get_panic_message(e.into_panic());
                error!(
                    "[Downloader] A download task panicked: {}. This job's outcome was not sent.",
                    panic_msg
                );
                critical_spawn_errors.push((
                    "[UnknownDownloadTaskPanic]".to_string(),
                    SpsError::Generic(format!("A download task panicked: {panic_msg}")),
                ));
            }
        }
        self.event_tx = None;
        critical_spawn_errors
    }
}

use std::sync::Arc;

use clap::Args;
use colored::Colorize;
use prettytable::{format, Cell, Row, Table};
use serde_json::Value;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result;
use sps_common::formulary::Formulary;
use sps_core::check::installed::{get_installed_packages, PackageType};
use sps_core::check::update::check_for_updates;
use sps_core::check::InstalledPackageInfo;

#[derive(Args, Debug)]
pub struct List {
    /// Show only formulas
    #[arg(long = "formula")]
    pub formula_only: bool,
    /// Show only casks
    #[arg(long = "cask")]
    pub cask_only: bool,
    /// Show only packages with updates available
    #[arg(long = "outdated")]
    pub outdated_only: bool,
}

impl List {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        let installed = get_installed_packages(config).await?;
        // Only show the latest version for each name
        use std::collections::HashMap;
        let mut formula_map: HashMap<&str, &sps_core::check::installed::InstalledPackageInfo> =
            HashMap::new();
        let mut cask_map: HashMap<&str, &sps_core::check::installed::InstalledPackageInfo> =
            HashMap::new();
        for pkg in &installed {
            match pkg.pkg_type {
                PackageType::Formula => {
                    let entry = formula_map.entry(pkg.name.as_str()).or_insert(pkg);
                    if pkg.version > entry.version {
                        formula_map.insert(pkg.name.as_str(), pkg);
                    }
                }
                PackageType::Cask => {
                    let entry = cask_map.entry(pkg.name.as_str()).or_insert(pkg);
                    if pkg.version > entry.version {
                        cask_map.insert(pkg.name.as_str(), pkg);
                    }
                }
            }
        }
        let mut formulas: Vec<&InstalledPackageInfo> = formula_map.values().copied().collect();
        let mut casks: Vec<&InstalledPackageInfo> = cask_map.values().copied().collect();
        // Sort formulas and casks alphabetically by name, then version
        formulas.sort_by(|a, b| a.name.cmp(&b.name).then(a.version.cmp(&b.version)));
        casks.sort_by(|a, b| a.name.cmp(&b.name).then(a.version.cmp(&b.version)));
        // If Nothing Installed.
        if formulas.is_empty() && casks.is_empty() {
            println!("{}", "0 formulas and casks installed".yellow());
            return Ok(());
        }
        // If user wants to show installed formulas only.
        if self.formula_only {
            if self.outdated_only {
                self.print_outdated_formulas_table(&formulas, config)
                    .await?;
            } else {
                self.print_formulas_table(formulas, config);
            }
            return Ok(());
        }
        // If user wants to show installed casks only.
        if self.cask_only {
            if self.outdated_only {
                self.print_outdated_casks_table(&casks, cache.clone())
                    .await?;
            } else {
                self.print_casks_table(casks, cache);
            }
            return Ok(());
        }

        // If user wants to show only outdated packages
        if self.outdated_only {
            self.print_outdated_all_table(&formulas, &casks, config, cache)
                .await?;
            return Ok(());
        }

        // Default Implementation
        let formulary = Formulary::new(config.clone());
        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        table.add_row(Row::new(vec![
            Cell::new("Type").style_spec("b"),
            Cell::new("Name").style_spec("b"),
            Cell::new("Installed").style_spec("b"),
            Cell::new("New Version?").style_spec("b"),
        ]));
        let mut formula_count = 0;
        let mut cask_count = 0;
        for pkg in formulas {
            let latest = formulary.load_formula(&pkg.name).ok();
            let (has_new, _) = match latest {
                Some(ref f) => {
                    let latest_version = f.version_str_full();
                    (latest_version != pkg.version, latest_version)
                }
                None => (false, "-".to_string()),
            };
            table.add_row(Row::new(vec![
                Cell::new("Formula").style_spec("Fg"),
                Cell::new(&pkg.name).style_spec("Fb"),
                Cell::new(&pkg.version),
                // TODO: update to display the latest version string.
                // TODO: Not showing when the using --all flag.
                Cell::new(if has_new { "✔" } else { "" }),
            ]));
            formula_count += 1;
        }
        for pkg in casks {
            // Try to load cask info from cache
            let cask_val = cache.load_raw("cask.json").ok().and_then(|raw| {
                serde_json::from_str::<Vec<Value>>(&raw)
                    .ok()?
                    .into_iter()
                    .find(|v| v.get("token").and_then(|t| t.as_str()) == Some(&pkg.name))
            });
            let (has_new, _) = match cask_val {
                Some(ref v) => {
                    let latest_version = v.get("version").and_then(|v| v.as_str()).unwrap_or("-");
                    (latest_version != pkg.version, latest_version.to_string())
                }
                None => (false, "-".to_string()),
            };
            table.add_row(Row::new(vec![
                Cell::new("Cask").style_spec("Fy"),
                Cell::new(&pkg.name).style_spec("Fb"),
                Cell::new(&pkg.version),
                Cell::new(if has_new { "✔" } else { "" }),
            ]));
            cask_count += 1;
        }
        table.printstd();
        if formula_count > 0 && cask_count > 0 {
            println!(
                "{}",
                format!("{formula_count} formulas, {cask_count} casks installed").bold()
            );
        } else if formula_count > 0 {
            println!("{}", format!("{formula_count} formulas installed").bold());
        } else if cask_count > 0 {
            println!("{}", format!("{cask_count} casks installed").bold());
        }
        Ok(())
    }

    fn print_formulas_table(
        &self,
        formulas: Vec<&sps_core::check::installed::InstalledPackageInfo>,
        config: &Config,
    ) {
        if formulas.is_empty() {
            println!("No formulas installed.");
            return;
        }
        let formulary = Formulary::new(config.clone());
        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        // Add header row with "Formulas" spanning all columns, font color green
        table.add_row(Row::new(vec![Cell::new_align(
            "Formulas",
            format::Alignment::CENTER,
        )
        .style_spec("bFg")
        .with_hspan(3)]));
        table.add_row(Row::new(vec![
            Cell::new("Name").style_spec("b"),
            Cell::new("Installed").style_spec("b"),
            Cell::new("New Version?").style_spec("b"),
        ]));
        let mut formula_count = 0;
        for pkg in formulas {
            let latest = formulary.load_formula(&pkg.name).ok();
            let (has_new, _) = match latest {
                Some(ref f) => {
                    let latest_version = f.version_str_full();
                    (latest_version != pkg.version, latest_version)
                }
                None => (false, "-".to_string()),
            };
            table.add_row(Row::new(vec![
                Cell::new(&pkg.name).style_spec("Fb"),
                Cell::new(&pkg.version),
                Cell::new(if has_new { "✔" } else { "" }),
            ]));
            formula_count += 1;
        }
        table.printstd();
        println!("{}", format!("{formula_count} formulas installed").bold());
    }

    fn print_casks_table(
        &self,
        casks: Vec<&sps_core::check::installed::InstalledPackageInfo>,
        cache: Arc<Cache>,
    ) {
        if casks.is_empty() {
            println!("No casks installed.");
            return;
        }
        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        // Add header row with "Casks" spanning all columns, font color green
        table.add_row(Row::new(vec![Cell::new_align(
            "Casks",
            format::Alignment::CENTER,
        )
        .style_spec("bFg")
        .with_hspan(3)]));
        table.add_row(Row::new(vec![
            Cell::new("Name").style_spec("b"),
            Cell::new("Installed").style_spec("b"),
            Cell::new("New Version?").style_spec("b"),
        ]));
        let mut cask_count = 0;
        for pkg in casks {
            // Try to load cask info from cache
            let cask_val = cache.load_raw("cask.json").ok().and_then(|raw| {
                serde_json::from_str::<Vec<Value>>(&raw)
                    .ok()?
                    .into_iter()
                    .find(|v| v.get("token").and_then(|t| t.as_str()) == Some(&pkg.name))
            });
            let (has_new, _) = match cask_val {
                Some(ref v) => {
                    let latest_version = v.get("version").and_then(|v| v.as_str()).unwrap_or("-");
                    (latest_version != pkg.version, latest_version.to_string())
                }
                None => (false, "-".to_string()),
            };
            table.add_row(Row::new(vec![
                Cell::new(&pkg.name).style_spec("Fb"),
                Cell::new(&pkg.version),
                Cell::new(if has_new { "✔" } else { "" }),
            ]));
            cask_count += 1;
        }
        table.printstd();
        println!("{}", format!("{cask_count} casks installed").bold());
    }

    async fn print_outdated_formulas_table(
        &self,
        formulas: &[&InstalledPackageInfo],
        config: &Config,
    ) -> Result<()> {
        if formulas.is_empty() {
            println!("No formulas installed.");
            return Ok(());
        }

        // Convert to owned for update checking
        let formula_packages: Vec<InstalledPackageInfo> =
            formulas.iter().map(|&f| f.clone()).collect();
        let cache = sps_common::cache::Cache::new(config)?;
        let updates = check_for_updates(&formula_packages, &cache, config).await?;

        if updates.is_empty() {
            println!("No formula updates available.");
            return Ok(());
        }

        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        table.add_row(Row::new(vec![Cell::new_align(
            "Outdated Formulas",
            format::Alignment::CENTER,
        )
        .style_spec("bFg")
        .with_hspan(3)]));
        table.add_row(Row::new(vec![
            Cell::new("Name").style_spec("b"),
            Cell::new("Installed").style_spec("b"),
            Cell::new("Available").style_spec("b"),
        ]));

        let mut count = 0;
        for update in updates {
            table.add_row(Row::new(vec![
                Cell::new(&update.name).style_spec("Fb"),
                Cell::new(&update.installed_version),
                Cell::new(&update.available_version).style_spec("Fg"),
            ]));
            count += 1;
        }

        table.printstd();
        println!("{}", format!("{count} outdated formulas").bold());
        Ok(())
    }

    async fn print_outdated_casks_table(
        &self,
        casks: &[&InstalledPackageInfo],
        cache: Arc<Cache>,
    ) -> Result<()> {
        if casks.is_empty() {
            println!("No casks installed.");
            return Ok(());
        }

        // Convert to owned for update checking
        let cask_packages: Vec<InstalledPackageInfo> = casks.iter().map(|&c| c.clone()).collect();
        let config = cache.config();
        let updates = check_for_updates(&cask_packages, &cache, config).await?;

        if updates.is_empty() {
            println!("No cask updates available.");
            return Ok(());
        }

        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        table.add_row(Row::new(vec![Cell::new_align(
            "Outdated Casks",
            format::Alignment::CENTER,
        )
        .style_spec("bFg")
        .with_hspan(3)]));
        table.add_row(Row::new(vec![
            Cell::new("Name").style_spec("b"),
            Cell::new("Installed").style_spec("b"),
            Cell::new("Available").style_spec("b"),
        ]));

        let mut count = 0;
        for update in updates {
            table.add_row(Row::new(vec![
                Cell::new(&update.name).style_spec("Fb"),
                Cell::new(&update.installed_version),
                Cell::new(&update.available_version).style_spec("Fy"),
            ]));
            count += 1;
        }

        table.printstd();
        println!("{}", format!("{count} outdated casks").bold());
        Ok(())
    }

    async fn print_outdated_all_table(
        &self,
        formulas: &[&InstalledPackageInfo],
        casks: &[&InstalledPackageInfo],
        config: &Config,
        cache: Arc<Cache>,
    ) -> Result<()> {
        // Convert to owned for update checking
        let mut all_packages: Vec<InstalledPackageInfo> = Vec::new();
        all_packages.extend(formulas.iter().map(|&f| f.clone()));
        all_packages.extend(casks.iter().map(|&c| c.clone()));

        if all_packages.is_empty() {
            println!("{}", "0 formulas and casks installed".yellow());
            return Ok(());
        }

        let updates = check_for_updates(&all_packages, &cache, config).await?;

        if updates.is_empty() {
            println!("No outdated packages found.");
            return Ok(());
        }

        let mut table = Table::new();
        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
        table.add_row(Row::new(vec![
            Cell::new("Type").style_spec("b"),
            Cell::new("Name").style_spec("b"),
            Cell::new("Installed").style_spec("b"),
            Cell::new("Available").style_spec("b"),
        ]));

        let mut formula_count = 0;
        let mut cask_count = 0;

        for update in updates {
            let (type_name, type_style) = match update.pkg_type {
                PackageType::Formula => {
                    formula_count += 1;
                    ("Formula", "Fg")
                }
                PackageType::Cask => {
                    cask_count += 1;
                    ("Cask", "Fy")
                }
            };

            table.add_row(Row::new(vec![
                Cell::new(type_name).style_spec(type_style),
                Cell::new(&update.name).style_spec("Fb"),
                Cell::new(&update.installed_version),
                Cell::new(&update.available_version).style_spec("Fg"),
            ]));
        }

        table.printstd();
        if formula_count > 0 && cask_count > 0 {
            println!(
                "{}",
                format!("{formula_count} outdated formulas, {cask_count} outdated casks").bold()
            );
        } else if formula_count > 0 {
            println!("{}", format!("{formula_count} outdated formulas").bold());
        } else if cask_count > 0 {
            println!("{}", format!("{cask_count} outdated casks").bold());
        }
        Ok(())
    }
}

// sps/src/pipeline/runner.rs
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;

use colored::Colorize;
use crossbeam_channel::bounded as crossbeam_bounded;
use reqwest::Client as HttpClient;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::dependency::resolver::{ResolutionStatus, ResolvedGraph};
use sps_common::error::{Result as SpsResult, SpsError};
use sps_common::model::InstallTargetIdentifier;
use sps_common::pipeline::{
    DownloadOutcome, JobProcessingState, PipelineEvent, PlannedJob,
    PlannedOperations as PlannerOutputCommon, WorkerJob,
};
use tokio::sync::{broadcast, mpsc};
use tokio::task::JoinHandle;
use tracing::{debug, error, instrument, warn};

use super::downloader::DownloadCoordinator;
use super::planner::OperationPlanner;

const WORKER_JOB_CHANNEL_SIZE: usize = 100;
const EVENT_CHANNEL_SIZE: usize = 100;
const DOWNLOAD_OUTCOME_CHANNEL_SIZE: usize = 100;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandType {
    Install,
    Reinstall,
    Upgrade { all: bool },
}

#[derive(Debug, Clone)]
pub struct PipelineFlags {
    pub build_from_source: bool,
    pub include_optional: bool,
    pub skip_recommended: bool,
}

struct PropagationContext {
    all_planned_jobs: Arc<Vec<PlannedJob>>,
    job_states: Arc<Mutex<HashMap<String, JobProcessingState>>>,
    resolved_graph: Arc<ResolvedGraph>,
    event_tx: Option<broadcast::Sender<PipelineEvent>>,
    final_fail_count: Arc<AtomicUsize>,
}

fn err_to_string(e: &SpsError) -> String {
    e.to_string()
}

pub(crate) fn get_panic_message(e: Box<dyn std::any::Any + Send>) -> String {
    match e.downcast_ref::<&'static str>() {
        Some(s) => (*s).to_string(),
        None => match e.downcast_ref::<String>() {
            Some(s) => s.clone(),
            None => "Unknown panic payload".to_string(),
        },
    }
}

#[instrument(skip_all, fields(cmd = ?command_type, targets = ?initial_targets))]
pub async fn run_pipeline(
    initial_targets: &[String],
    command_type: CommandType,
    config: &Config,
    cache: Arc<Cache>,
    flags: &PipelineFlags,
) -> SpsResult<()> {
    debug!(
        "Pipeline run initiated for targets: {:?}, command: {:?}",
        initial_targets, command_type
    );
    let start_time = Instant::now();
    let final_success_count = Arc::new(AtomicUsize::new(0));
    let final_fail_count = Arc::new(AtomicUsize::new(0));

    debug!(
        "Creating broadcast channel for pipeline events (EVENT_CHANNEL_SIZE={})",
        EVENT_CHANNEL_SIZE
    );
    let (event_tx, mut event_rx_for_runner) =
        broadcast::channel::<PipelineEvent>(EVENT_CHANNEL_SIZE);

    debug!("Cloning event_tx for runner_event_tx_clone");
    let runner_event_tx_clone = event_tx.clone();

    debug!(
        "Creating crossbeam worker job channel (WORKER_JOB_CHANNEL_SIZE={})",
        WORKER_JOB_CHANNEL_SIZE
    );
    let (worker_job_tx, worker_job_rx_for_core) =
        crossbeam_bounded::<WorkerJob>(WORKER_JOB_CHANNEL_SIZE);

    debug!("Cloning event_tx for core_event_tx_for_worker_manager");
    let core_config = config.clone();
    let core_cache_clone = cache.clone();
    let core_event_tx_for_worker_manager = event_tx.clone();
    let core_success_count_clone = Arc::clone(&final_success_count);
    let core_fail_count_clone = Arc::clone(&final_fail_count);
    debug!("Spawning core worker pool manager thread.");
    let core_handle = std::thread::spawn(move || {
        debug!("CORE_THREAD: Core worker pool manager thread started.");
        let result = sps_core::pipeline::engine::start_worker_pool_manager(
            core_config,
            core_cache_clone,
            worker_job_rx_for_core,
            core_event_tx_for_worker_manager,
            core_success_count_clone,
            core_fail_count_clone,
        );
        debug!(
            "CORE_THREAD: Core worker pool manager thread finished. Result: {:?}",
            result.is_ok()
        );
        result
    });

    debug!("Subscribing to event_tx for status_event_rx");
    let status_config = config.clone();
    let status_event_rx = event_tx.subscribe();
    debug!("Spawning status handler task.");
    let status_handle = tokio::spawn(crate::cli::status::handle_events(
        status_config,
        status_event_rx,
    ));

    debug!(
        "Creating mpsc download_outcome channel (DOWNLOAD_OUTCOME_CHANNEL_SIZE={})",
        DOWNLOAD_OUTCOME_CHANNEL_SIZE
    );
    let (download_outcome_tx, mut download_outcome_rx) =
        mpsc::channel::<DownloadOutcome>(DOWNLOAD_OUTCOME_CHANNEL_SIZE);

    debug!("Initializing pipeline planning phase...");
    let planner_output: PlannerOutputCommon;
    {
        debug!("Cloning runner_event_tx_clone for planner_event_tx_clone");
        let planner_event_tx_clone = runner_event_tx_clone.clone();
        debug!("Creating OperationPlanner.");
        let operation_planner =
            OperationPlanner::new(config, cache.clone(), flags, planner_event_tx_clone);

        debug!("Calling plan_operations...");
        match operation_planner
            .plan_operations(initial_targets, command_type.clone())
            .await
        {
            Ok(ops) => {
                debug!("plan_operations returned Ok.");
                planner_output = ops;
            }
            Err(e) => {
                error!("Fatal planning error: {}", e);
                runner_event_tx_clone
                    .send(PipelineEvent::LogError {
                        message: format!("Fatal planning error: {e}"),
                    })
                    .ok();
                drop(worker_job_tx);
                if let Err(join_err) = core_handle.join() {
                    error!(
                        "Core thread join error after planning failure: {:?}",
                        get_panic_message(join_err)
                    );
                }
                let duration = start_time.elapsed();
                runner_event_tx_clone
                    .send(PipelineEvent::PipelineFinished {
                        duration_secs: duration.as_secs_f64(),
                        success_count: 0,
                        fail_count: initial_targets.len(),
                    })
                    .ok();

                debug!("Dropping runner_event_tx_clone due to planning error.");
                drop(runner_event_tx_clone);
                debug!("Dropping main event_tx due to planning error.");
                drop(event_tx);

                debug!("Awaiting status_handle after planning error.");
                if let Err(join_err) = status_handle.await {
                    error!(
                        "Status task join error after planning failure: {}",
                        join_err
                    );
                }
                return Err(e);
            }
        }
        debug!("OperationPlanner scope ended, planner_event_tx_clone dropped.");
    }

    let planned_jobs = Arc::new(planner_output.jobs);
    let resolved_graph = planner_output.resolved_graph.clone()
        .unwrap_or_else(|| {
            tracing::debug!("ResolvedGraph was None in planner output. Using a default empty graph. This is expected if no formulae required resolution or if planner reported errors for all formulae.");
            Arc::new(sps_common::dependency::resolver::ResolvedGraph::default())
        });

    debug!(
        "Planning finished. Total jobs in plan: {}.",
        planned_jobs.len()
    );
    runner_event_tx_clone
        .send(PipelineEvent::PlanningFinished {
            job_count: planned_jobs.len(),
        })
        .ok();

    // Mark jobs with planner errors as failed and emit error events
    let job_processing_states = Arc::new(Mutex::new(HashMap::<String, JobProcessingState>::new()));
    let mut jobs_pending_or_active = 0;
    let mut initial_fail_count_from_planner = 0;
    {
        let mut states_guard = job_processing_states.lock().unwrap();
        if !planner_output.errors.is_empty() {
            tracing::debug!(
                "[Runner] Planner reported {} error(s). These targets will be marked as failed.",
                planner_output.errors.len()
            );
            for (target_name, error) in &planner_output.errors {
                let msg = format!("✗ {}: {}", target_name.cyan(), error);
                runner_event_tx_clone
                    .send(PipelineEvent::LogError { message: msg })
                    .ok();
                states_guard.insert(
                    target_name.clone(),
                    JobProcessingState::Failed(Arc::new(error.clone())),
                );
                initial_fail_count_from_planner += 1;
            }
        }
        for job in planned_jobs.iter() {
            if states_guard.contains_key(&job.target_id) {
                continue;
            }
            if planner_output
                .already_installed_or_up_to_date
                .contains(&job.target_id)
            {
                states_guard.insert(job.target_id.clone(), JobProcessingState::Succeeded);
                final_success_count.fetch_add(1, Ordering::Relaxed);
                debug!(
                    "[{}] Marked as Succeeded (pre-existing/up-to-date).",
                    job.target_id
                );
            } else if let Some((_, err)) = planner_output
                .errors
                .iter()
                .find(|(name, _)| name == &job.target_id)
            {
                states_guard.insert(
                    job.target_id.clone(),
                    JobProcessingState::Failed(Arc::new(err.clone())),
                );
                // Counted in initial_fail_count_from_planner
                debug!(
                    "[{}] Marked as Failed (planning error: {}).",
                    job.target_id, err
                );
            } else if job.use_private_store_source.is_some() {
                let path = job.use_private_store_source.clone().unwrap();
                states_guard.insert(
                    job.target_id.clone(),
                    JobProcessingState::Downloaded(path.clone()),
                );
                jobs_pending_or_active += 1;
                debug!(
                    "[{}] Initial state: Downloaded (private store: {}). Active jobs: {}",
                    job.target_id,
                    path.display(),
                    jobs_pending_or_active
                );
            } else {
                states_guard.insert(job.target_id.clone(), JobProcessingState::PendingDownload);
                jobs_pending_or_active += 1;
                debug!(
                    "[{}] Initial state: PendingDownload. Active jobs: {}",
                    job.target_id, jobs_pending_or_active
                );
            }
        }
    }
    debug!(
        "Initial job states populated. Jobs pending/active: {}",
        jobs_pending_or_active
    );

    let mut downloads_to_initiate = Vec::new();
    {
        let states_guard = job_processing_states.lock().unwrap();
        for job in planned_jobs.iter() {
            if matches!(
                states_guard.get(&job.target_id),
                Some(JobProcessingState::PendingDownload)
            ) {
                downloads_to_initiate.push(job.clone());
            }
        }
    }

    let mut download_coordinator_task_handle: Option<JoinHandle<Vec<(String, SpsError)>>> = None;

    if !downloads_to_initiate.is_empty() {
        debug!("Cloning runner_event_tx_clone for download_coordinator_event_tx_clone");
        let download_coordinator_event_tx_clone = runner_event_tx_clone.clone();
        let http_client = Arc::new(HttpClient::new());
        let config_for_downloader_owned = config.clone();

        let mut download_coordinator = DownloadCoordinator::new(
            config_for_downloader_owned,
            cache.clone(),
            http_client,
            download_coordinator_event_tx_clone,
        );
        debug!(
            "Starting download coordination for {} jobs...",
            downloads_to_initiate.len()
        );
        debug!("Cloning download_outcome_tx for tx_for_download_task");
        let tx_for_download_task = download_outcome_tx.clone();

        debug!("Spawning DownloadCoordinator task.");
        download_coordinator_task_handle = Some(tokio::spawn(async move {
            debug!("DOWNLOAD_COORD_TASK: DownloadCoordinator task started.");
            let result = download_coordinator
                .coordinate_downloads(downloads_to_initiate, tx_for_download_task)
                .await;
            debug!("DOWNLOAD_COORD_TASK: DownloadCoordinator task finished. coordinate_downloads returned.");
            result
        }));
    } else if jobs_pending_or_active > 0 {
        debug!(
            "No downloads to initiate, but {} jobs are pending. Triggering check_and_dispatch.",
            jobs_pending_or_active
        );
        check_and_dispatch(
            planned_jobs.clone(),
            job_processing_states.clone(),
            resolved_graph.clone(),
            &worker_job_tx,
            runner_event_tx_clone.clone(),
            config,
            flags,
        );
    } else {
        debug!("No downloads to initiate and no jobs pending/active. Pipeline might be empty or all pre-satisfied/failed.");
    }

    drop(download_outcome_tx);
    debug!("Dropped main MPSC download_outcome_tx (runner's original clone).");

    if !planned_jobs.is_empty() {
        runner_event_tx_clone
            .send(PipelineEvent::PipelineStarted {
                total_jobs: planned_jobs.len(),
            })
            .ok();
    }

    let mut propagation_ctx = PropagationContext {
        all_planned_jobs: planned_jobs.clone(),
        job_states: job_processing_states.clone(),
        resolved_graph: resolved_graph.clone(),
        event_tx: Some(runner_event_tx_clone.clone()),
        final_fail_count: final_fail_count.clone(),
    };

    debug!(
        "Entering main event loop. Jobs pending/active: {}",
        jobs_pending_or_active
    );

    // Robust main loop: continue while there are jobs pending/active, or downloads, or jobs in
    // states that could be dispatched
    fn has_pending_dispatchable_jobs(
        states_guard: &std::sync::MutexGuard<HashMap<String, JobProcessingState>>,
    ) -> bool {
        states_guard.values().any(|state| {
            matches!(
                state,
                JobProcessingState::Downloaded(_) | JobProcessingState::WaitingForDependencies(_)
            )
        })
    }

    while jobs_pending_or_active > 0
        || has_pending_dispatchable_jobs(&job_processing_states.lock().unwrap())
    {
        tokio::select! {
            biased;
            Some(download_outcome) = download_outcome_rx.recv() => {
                debug!("Received DownloadOutcome for '{}'.", download_outcome.planned_job.target_id);
                process_download_outcome(
                    download_outcome,
                    &propagation_ctx,
                    &mut jobs_pending_or_active,
                );
                debug!("After process_download_outcome, jobs_pending_or_active: {}. Triggering check_and_dispatch.", jobs_pending_or_active);
                 check_and_dispatch(
                    planned_jobs.clone(),
                    job_processing_states.clone(),
                    resolved_graph.clone(),
                    &worker_job_tx,
                    runner_event_tx_clone.clone(),
                    config,
                    flags,
                );
            }
            Ok(event) = event_rx_for_runner.recv() => {
                match event {
                    PipelineEvent::JobSuccess { ref target_id, .. } => {
                        debug!("Received JobSuccess for '{}'.", target_id);
                        process_core_worker_feedback(
                            target_id.clone(),
                            true,
                            None,
                            job_processing_states.clone(),
                            &mut jobs_pending_or_active,
                        );
                        debug!("After JobSuccess for '{}', jobs_pending_or_active: {}. Triggering check_and_dispatch.", target_id, jobs_pending_or_active);
                        check_and_dispatch(
                            planned_jobs.clone(),
                            job_processing_states.clone(),
                            resolved_graph.clone(),
                            &worker_job_tx,
                            runner_event_tx_clone.clone(),
                            config,
                            flags,
                        );
                    }
                    PipelineEvent::JobFailed { ref target_id, ref error, ref action } => {
                        debug!("Received JobFailed for '{}' (Action: {:?}, Error: {}).", target_id, action, error);
                        process_core_worker_feedback(
                            target_id.clone(),
                            false,
                            Some(SpsError::Generic(error.clone())),
                            job_processing_states.clone(),
                            &mut jobs_pending_or_active,
                        );
                        debug!("After JobFailed for '{}', jobs_pending_or_active: {}. Triggering failure propagation.", target_id, jobs_pending_or_active);
                        propagate_failure(
                            target_id,
                            Arc::new(SpsError::Generic(format!("Core worker failed for {target_id}: {error}"))),
                            &propagation_ctx,
                            &mut jobs_pending_or_active,
                        );
                         check_and_dispatch(
                            planned_jobs.clone(),
                            job_processing_states.clone(),
                            resolved_graph.clone(),
                            &worker_job_tx,
                            runner_event_tx_clone.clone(),
                            config,
                            flags,
                        );
                    }
                    _ => {}
                }
            }
            else => {
                debug!("Main select loop 'else' branch. jobs_pending_or_active = {}. download_outcome_rx or event_rx_for_runner might be closed.", jobs_pending_or_active);
                if jobs_pending_or_active > 0 || has_pending_dispatchable_jobs(&job_processing_states.lock().unwrap()) {
                    warn!("Exiting main loop prematurely but still have {} jobs pending/active or dispatchable. This might indicate a stall or logic error.", jobs_pending_or_active);
                }
                break;
            }
        }
        debug!(
            "End of select! loop iteration. Jobs pending/active: {}",
            jobs_pending_or_active
        );
    }
    debug!(
        "Main event loop finished. Final jobs_pending_or_active: {}",
        jobs_pending_or_active
    );

    drop(download_outcome_rx);
    debug!("Dropped MPSC download_outcome_rx (runner's receiver).");

    if let Some(handle) = download_coordinator_task_handle {
        debug!("Waiting for DownloadCoordinator task to complete...");
        match handle.await {
            Ok(critical_download_errors) => {
                if !critical_download_errors.is_empty() {
                    warn!(
                        "DownloadCoordinator task reported critical errors: {:?}",
                        critical_download_errors
                    );
                    final_fail_count.fetch_add(critical_download_errors.len(), Ordering::Relaxed);
                }
                debug!("DownloadCoordinator task completed.");
            }
            Err(e) => {
                let panic_msg = get_panic_message(Box::new(e));
                error!(
                    "DownloadCoordinator task panicked or failed to join: {}",
                    panic_msg
                );
                final_fail_count.fetch_add(1, Ordering::Relaxed);
            }
        }
    } else {
        debug!("No DownloadCoordinator task was spawned or it was already handled.");
    }
    debug!("DownloadCoordinator task processing finished (awaited or none).");

    debug!("Closing worker job channel (signal to core workers).");
    drop(worker_job_tx);
    debug!("Waiting for core worker pool to join...");
    match core_handle.join() {
        Ok(Ok(())) => debug!("Core worker pool manager thread completed successfully."),
        Ok(Err(e)) => {
            error!("Core worker pool manager thread failed: {}", e);
            final_fail_count.fetch_add(1, Ordering::Relaxed);
        }
        Err(e) => {
            error!(
                "Core worker pool manager thread panicked: {:?}",
                get_panic_message(e)
            );
            final_fail_count.fetch_add(1, Ordering::Relaxed);
        }
    }
    debug!("Core worker pool joined. core_event_tx_for_worker_manager (broadcast sender) dropped.");

    let duration = start_time.elapsed();
    let success_total = final_success_count.load(Ordering::Relaxed);
    let fail_total = final_fail_count.load(Ordering::Relaxed) + initial_fail_count_from_planner;

    debug!(
        "Pipeline processing finished. Success: {}, Fail: {}. Duration: {:.2}s. Sending PipelineFinished event.",
        success_total, fail_total, duration.as_secs_f64()
    );
    if let Err(e) = runner_event_tx_clone.send(PipelineEvent::PipelineFinished {
        duration_secs: duration.as_secs_f64(),
        success_count: success_total,
        fail_count: fail_total,
    }) {
        warn!(
            "Failed to send PipelineFinished event: {:?}. Status handler might not receive it.",
            e
        );
    }

    // Explicitly drop the event_tx inside propagation_ctx before dropping the last senders.
    propagation_ctx.event_tx = None;

    debug!("Dropping runner_event_tx_clone (broadcast sender).");
    drop(runner_event_tx_clone);
    // event_rx_for_runner (broadcast receiver) goes out of scope here and is dropped.

    debug!("Dropping main event_tx (final broadcast sender).");
    drop(event_tx);

    debug!("All known broadcast senders dropped. About to await status_handle.");
    if let Err(e) = status_handle.await {
        warn!("Status handler task failed or panicked: {}", e);
    } else {
        debug!("Status handler task completed successfully.");
    }
    debug!("run_pipeline function is ending.");

    if fail_total == 0 {
        Ok(())
    } else {
        let mut accumulated_errors = Vec::new();
        for (name, err_obj) in planner_output.errors {
            accumulated_errors.push(format!("Planning for '{name}': {err_obj}"));
        }
        let states_guard = job_processing_states.lock().unwrap();
        for job in planned_jobs.iter() {
            if let Some(JobProcessingState::Failed(err_arc)) = states_guard.get(&job.target_id) {
                let err_str = err_to_string(err_arc);
                let job_err_msg = format!("Processing '{}': {}", job.target_id, err_str);
                if !accumulated_errors.contains(&job_err_msg) {
                    accumulated_errors.push(job_err_msg);
                }
            }
        }
        drop(states_guard);

        let specific_error_msg = if accumulated_errors.is_empty() {
            "No specific errors logged, check core worker logs.".to_string()
        } else {
            accumulated_errors.join("; ")
        };

        // Error details are already sent via PipelineEvent::JobFailed events
        // and will be displayed in status.rs
        Err(SpsError::InstallError(format!(
            "Operation failed with {fail_total} total failure(s). Details: [{specific_error_msg}] (Worker errors are included in total)"
        )))
    }
}

fn process_download_outcome(
    outcome: DownloadOutcome,
    propagation_ctx: &PropagationContext,
    jobs_pending_or_active: &mut usize,
) {
    let job_id = outcome.planned_job.target_id.clone();
    let mut states_guard = propagation_ctx.job_states.lock().unwrap();

    match states_guard.get(&job_id) {
        Some(JobProcessingState::Succeeded) | Some(JobProcessingState::Failed(_)) => {
            debug!(
                "[{}] DownloadOutcome: Job already in terminal state {:?}. Ignoring outcome.",
                job_id,
                states_guard.get(&job_id)
            );
            return;
        }
        _ => {}
    }

    match outcome.result {
        Ok(path) => {
            debug!(
                "[{}] DownloadOutcome: Success. Path: {}. Updating state to Downloaded.",
                job_id,
                path.display()
            );
            states_guard.insert(job_id.clone(), JobProcessingState::Downloaded(path));
        }
        Err(e) => {
            warn!(
                "[{}] DownloadOutcome: Failed. Error: {}. Updating state to Failed.",
                job_id, e
            );
            let error_arc = Arc::new(e);
            states_guard.insert(
                job_id.clone(),
                JobProcessingState::Failed(error_arc.clone()),
            );

            if let Some(ref tx) = propagation_ctx.event_tx {
                tx.send(PipelineEvent::job_failed(
                    job_id.clone(),
                    outcome.planned_job.action.clone(),
                    &error_arc,
                ))
                .ok();
            }
            propagation_ctx
                .final_fail_count
                .fetch_add(1, Ordering::Relaxed);

            if *jobs_pending_or_active > 0 {
                *jobs_pending_or_active -= 1;
                debug!("[{}] DownloadOutcome: Decremented jobs_pending_or_active to {} due to download failure.", job_id, *jobs_pending_or_active);
            } else {
                warn!("[{}] DownloadOutcome: jobs_pending_or_active is already 0, cannot decrement for download failure.", job_id);
            }

            drop(states_guard);
            debug!("[{}] DownloadOutcome: Propagating failure.", job_id);
            propagate_failure(&job_id, error_arc, propagation_ctx, jobs_pending_or_active);
        }
    }
}

fn process_core_worker_feedback(
    target_id: String,
    success: bool,
    error: Option<SpsError>,
    job_states: Arc<Mutex<HashMap<String, JobProcessingState>>>,
    jobs_pending_or_active: &mut usize,
) {
    let mut states_guard = job_states.lock().unwrap();

    match states_guard.get(&target_id) {
        Some(JobProcessingState::Succeeded) | Some(JobProcessingState::Failed(_)) => {
            debug!("[{}] CoreFeedback: Job already in terminal state {:?}. Ignoring active job count update.", target_id, states_guard.get(&target_id));
            return;
        }
        _ => {}
    }

    if success {
        debug!(
            "[{}] CoreFeedback: Success. Updating state to Succeeded.",
            target_id
        );
        states_guard.insert(target_id.clone(), JobProcessingState::Succeeded);
    } else {
        let err_msg = error.as_ref().map_or_else(
            || "Unknown core worker error".to_string(),
            |e| e.to_string(),
        );
        debug!(
            "[{}] CoreFeedback: Failed. Error: {}. Updating state to Failed.",
            target_id, err_msg
        );
        let err_arc = Arc::new(
            error.unwrap_or_else(|| SpsError::Generic("Unknown core worker error".into())),
        );
        states_guard.insert(target_id.clone(), JobProcessingState::Failed(err_arc));
    }

    if *jobs_pending_or_active > 0 {
        *jobs_pending_or_active -= 1;
        debug!(
            "[{}] CoreFeedback: Decremented jobs_pending_or_active to {}.",
            target_id, *jobs_pending_or_active
        );
    } else {
        warn!(
            "[{}] CoreFeedback: jobs_pending_or_active is already 0, cannot decrement.",
            target_id
        );
    }
}

fn check_and_dispatch(
    planned_jobs_arc: Arc<Vec<PlannedJob>>,
    job_states: Arc<Mutex<HashMap<String, JobProcessingState>>>,
    resolved_graph: Arc<ResolvedGraph>,
    worker_job_tx: &crossbeam_channel::Sender<WorkerJob>,
    event_tx: broadcast::Sender<PipelineEvent>,
    config: &Config,
    flags: &PipelineFlags,
) {
    debug!("--- Enter check_and_dispatch ---");
    let mut states_guard = job_states.lock().unwrap();
    let mut dispatched_this_round = 0;

    for planned_job in planned_jobs_arc.iter() {
        let job_id = &planned_job.target_id;
        debug!("[{}] CheckDispatch: Evaluating job.", job_id);

        let (current_state_is_dispatchable, path_for_dispatch) = {
            match states_guard.get(job_id) {
                Some(JobProcessingState::Downloaded(ref path)) => {
                    debug!("[{}] CheckDispatch: Current state is Downloaded.", job_id);
                    (true, Some(path.clone()))
                }
                Some(JobProcessingState::WaitingForDependencies(ref path)) => {
                    debug!(
                        "[{}] CheckDispatch: Current state is WaitingForDependencies.",
                        job_id
                    );
                    (true, Some(path.clone()))
                }
                other_state => {
                    debug!(
                        "[{}] CheckDispatch: Not in a dispatchable state. Current state: {:?}.",
                        job_id,
                        other_state.map(|s| format!("{s:?}"))
                    );
                    (false, None)
                }
            }
        };

        if current_state_is_dispatchable {
            let path = path_for_dispatch.unwrap();
            drop(states_guard);
            debug!(
                "[{}] CheckDispatch: Calling are_dependencies_succeeded.",
                job_id
            );
            let dependencies_succeeded = are_dependencies_succeeded(
                job_id,
                &planned_job.target_definition,
                job_states.clone(),
                &resolved_graph,
                config,
                flags,
            );
            states_guard = job_states.lock().unwrap();
            debug!(
                "[{}] CheckDispatch: are_dependencies_succeeded returned: {}.",
                job_id, dependencies_succeeded
            );

            let current_state_after_dep_check = states_guard.get(job_id).cloned();
            if !matches!(
                current_state_after_dep_check,
                Some(JobProcessingState::Downloaded(_))
                    | Some(JobProcessingState::WaitingForDependencies(_))
            ) {
                debug!("[{}] CheckDispatch: State changed to {:?} while checking dependencies. Skipping dispatch.", job_id, current_state_after_dep_check);
                continue;
            }

            if dependencies_succeeded {
                debug!(
                    "[{}] CheckDispatch: All dependencies satisfied. Dispatching to core worker.",
                    job_id
                );
                let worker_job = WorkerJob {
                    request: planned_job.clone(),
                    download_path: path.clone(),
                    download_size_bytes: std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0),
                    is_source_from_private_store: planned_job.use_private_store_source.is_some(),
                };
                if worker_job_tx.send(worker_job).is_ok() {
                    states_guard.insert(
                        job_id.clone(),
                        JobProcessingState::DispatchedToCore(path.clone()),
                    );
                    event_tx
                        .send(PipelineEvent::JobDispatchedToCore {
                            target_id: job_id.clone(),
                        })
                        .ok();
                    dispatched_this_round += 1;
                    debug!("[{}] CheckDispatch: Successfully dispatched.", job_id);
                } else {
                    error!("[{}] CheckDispatch: Failed to send job to worker channel (channel closed?). Marking as failed.", job_id);
                    let err = Arc::new(SpsError::Generic("Worker channel closed".to_string()));
                    if !matches!(
                        states_guard.get(job_id),
                        Some(JobProcessingState::Failed(_))
                    ) {
                        states_guard
                            .insert(job_id.clone(), JobProcessingState::Failed(err.clone()));
                        event_tx
                            .send(PipelineEvent::job_failed(
                                job_id.clone(),
                                planned_job.action.clone(),
                                &err,
                            ))
                            .ok();
                    }
                }
            } else if matches!(
                current_state_after_dep_check,
                Some(JobProcessingState::Downloaded(_))
            ) {
                debug!("[{}] CheckDispatch: Dependencies not met. Updating state to WaitingForDependencies.", job_id);
                states_guard.insert(
                    job_id.clone(),
                    JobProcessingState::WaitingForDependencies(path.clone()),
                );
            } else {
                debug!(
                    "[{}] CheckDispatch: Dependencies not met. State remains {:?}.",
                    job_id, current_state_after_dep_check
                );
            }
        }
    }
    if dispatched_this_round > 0 {
        debug!(
            "Dispatched {} jobs to core workers in this round.",
            dispatched_this_round
        );
    }
    debug!("--- Exit check_and_dispatch ---");
}

fn are_dependencies_succeeded(
    target_id: &str,
    target_def: &InstallTargetIdentifier,
    job_states_arc: Arc<Mutex<HashMap<String, JobProcessingState>>>,
    resolved_graph: &ResolvedGraph,
    config: &Config,
    flags: &PipelineFlags,
) -> bool {
    debug!("[{}] AreDepsSucceeded: Checking dependencies...", target_id);
    let dependencies_to_check: Vec<String> = match target_def {
        InstallTargetIdentifier::Formula(formula_arc) => {
            if let Some(resolved_dep_info) =
                resolved_graph.resolution_details.get(formula_arc.name())
            {
                let parent_strategy = resolved_dep_info.determined_install_strategy;
                let empty_actions = std::collections::HashMap::new();
                let context = sps_common::dependency::ResolutionContext {
                    formulary: &sps_common::formulary::Formulary::new(config.clone()),
                    keg_registry: &sps_common::keg::KegRegistry::new(config.clone()),
                    sps_prefix: config.sps_root(),
                    include_optional: flags.include_optional,
                    include_test: false,
                    skip_recommended: flags.skip_recommended,
                    initial_target_preferences: &Default::default(),
                    build_all_from_source: flags.build_from_source,
                    cascade_source_preference_to_dependencies: true,
                    has_bottle_for_current_platform:
                        sps_core::install::bottle::has_bottle_for_current_platform,
                    initial_target_actions: &empty_actions,
                };

                let deps: Vec<String> = formula_arc
                    .dependencies()
                    .unwrap_or_default()
                    .iter()
                    .filter(|dep_edge| {
                        context.should_process_dependency_edge(
                            formula_arc,
                            dep_edge.tags,
                            parent_strategy,
                        )
                    })
                    .map(|dep_edge| dep_edge.name.clone())
                    .collect();
                debug!(
                    "[{}] AreDepsSucceeded: Formula dependencies to check: {:?}",
                    target_id, deps
                );
                deps
            } else {
                warn!("[{}] AreDepsSucceeded: Formula not found in ResolvedGraph. Assuming no dependencies.", target_id);
                Vec::new()
            }
        }
        InstallTargetIdentifier::Cask(cask_arc) => {
            let deps = if let Some(deps_on) = &cask_arc.depends_on {
                deps_on.formula.clone()
            } else {
                Vec::new()
            };
            debug!(
                "[{}] AreDepsSucceeded: Cask formula dependencies to check: {:?}",
                target_id, deps
            );
            deps
        }
    };

    if dependencies_to_check.is_empty() {
        debug!(
            "[{}] AreDepsSucceeded: No dependencies to check. Returning true.",
            target_id
        );
        return true;
    }

    let states_guard = job_states_arc.lock().unwrap();
    for dep_name in &dependencies_to_check {
        match states_guard.get(dep_name) {
            Some(JobProcessingState::Succeeded) => {
                debug!(
                    "[{}] AreDepsSucceeded: Dependency '{}' is Succeeded.",
                    target_id, dep_name
                );
            }
            Some(JobProcessingState::Failed(err)) => {
                debug!(
                    "[{}] AreDepsSucceeded: Dependency '{}' is FAILED ({}). Returning false.",
                    target_id,
                    dep_name,
                    err_to_string(err)
                );
                return false;
            }
            None => {
                if let Some(resolved_dep_detail) = resolved_graph.resolution_details.get(dep_name) {
                    if resolved_dep_detail.status == ResolutionStatus::Installed {
                        debug!("[{}] AreDepsSucceeded: Dependency '{}' is already installed (from ResolvedGraph).", target_id, dep_name);
                    } else {
                        debug!("[{}] AreDepsSucceeded: Dependency '{}' has no active state and not ResolvedGraph::Installed (is {:?}). Returning false.", target_id, dep_name, resolved_dep_detail.status);
                        return false;
                    }
                } else {
                    warn!("[{}] AreDepsSucceeded: Dependency '{}' not found in job_states OR ResolvedGraph. Assuming not met. Returning false.", target_id, dep_name);
                    return false;
                }
            }
            other_state => {
                debug!("[{}] AreDepsSucceeded: Dependency '{}' is not yet Succeeded. Current state: {:?}. Returning false.", target_id, dep_name, other_state.map(|s| format!("{s:?}")));
                return false;
            }
        }
    }
    debug!(
        "[{}] AreDepsSucceeded: All dependencies Succeeded or were pre-installed. Returning true.",
        target_id
    );
    true
}

fn propagate_failure(
    failed_job_id: &str,
    failure_reason: Arc<SpsError>,
    ctx: &PropagationContext,
    jobs_pending_or_active: &mut usize,
) {
    debug!(
        "[{}] PropagateFailure: Starting for reason: {}",
        failed_job_id, failure_reason
    );
    let mut dependents_to_fail_queue = vec![failed_job_id.to_string()];
    let mut newly_failed_dependents = HashSet::new();

    {
        let mut states_guard = ctx.job_states.lock().unwrap();
        if !matches!(
            states_guard.get(failed_job_id),
            Some(JobProcessingState::Failed(_))
        ) {
            states_guard.insert(
                failed_job_id.to_string(),
                JobProcessingState::Failed(failure_reason.clone()),
            );
        }
    }

    let mut current_idx = 0;
    while current_idx < dependents_to_fail_queue.len() {
        let current_source_of_failure = dependents_to_fail_queue[current_idx].clone();
        current_idx += 1;

        for job_to_check in ctx.all_planned_jobs.iter() {
            if job_to_check.target_id == failed_job_id
                || newly_failed_dependents.contains(&job_to_check.target_id)
            {
                continue;
            }

            let is_dependent = match &job_to_check.target_definition {
                InstallTargetIdentifier::Formula(formula_arc) => ctx
                    .resolved_graph
                    .resolution_details
                    .get(formula_arc.name())
                    .is_some_and(|res_dep_info| {
                        res_dep_info
                            .formula
                            .dependencies()
                            .unwrap_or_default()
                            .iter()
                            .any(|d| d.name == current_source_of_failure)
                    }),
                InstallTargetIdentifier::Cask(cask_arc) => {
                    cask_arc.depends_on.as_ref().is_some_and(|deps| {
                        deps.formula.contains(&current_source_of_failure)
                            || deps.cask.contains(&current_source_of_failure)
                    })
                }
            };

            if is_dependent {
                let mut states_guard = ctx.job_states.lock().unwrap();
                let current_state_of_dependent = states_guard.get(&job_to_check.target_id).cloned();

                if !matches!(
                    current_state_of_dependent,
                    Some(JobProcessingState::Succeeded) | Some(JobProcessingState::Failed(_))
                ) {
                    let propagated_error = Arc::new(SpsError::DependencyError(format!(
                        "Dependency '{}' failed: {}",
                        current_source_of_failure,
                        err_to_string(&failure_reason)
                    )));
                    states_guard.insert(
                        job_to_check.target_id.clone(),
                        JobProcessingState::Failed(propagated_error.clone()),
                    );

                    if newly_failed_dependents.insert(job_to_check.target_id.clone()) {
                        dependents_to_fail_queue.push(job_to_check.target_id.clone());
                        ctx.final_fail_count.fetch_add(1, Ordering::Relaxed);

                        if *jobs_pending_or_active > 0 {
                            *jobs_pending_or_active -= 1;
                            debug!("[{}] PropagateFailure: Decremented jobs_pending_or_active to {} for propagated failure.", job_to_check.target_id, *jobs_pending_or_active);
                        } else {
                            warn!("[{}] PropagateFailure: jobs_pending_or_active is already 0, cannot decrement for propagated failure.", job_to_check.target_id);
                        }

                        if let Some(ref tx) = ctx.event_tx {
                            tx.send(PipelineEvent::job_failed(
                                job_to_check.target_id.clone(),
                                job_to_check.action.clone(),
                                &propagated_error,
                            ))
                            .ok();
                        }
                        debug!("[{}] PropagateFailure: Marked as FAILED due to propagated failure from '{}'.", job_to_check.target_id, current_source_of_failure);
                    }
                }
                drop(states_guard);
            }
        }
    }

    if !newly_failed_dependents.is_empty() {
        debug!(
            "[{}] PropagateFailure: Finished. Newly failed dependents: {:?}",
            failed_job_id, newly_failed_dependents
        );
    } else {
        debug!(
            "[{}] PropagateFailure: Finished. No new dependents marked as failed.",
            failed_job_id
        );
    }
}

use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use futures::StreamExt;
use reqwest::header::{HeaderMap, ACCEPT, USER_AGENT};
use reqwest::{Client, StatusCode};
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::formula::ResourceSpec;
use tokio::fs::File as TokioFile;
use tokio::io::AsyncWriteExt;
use tracing::{debug, error};

use crate::validation::{validate_url, verify_checksum};

pub type ProgressCallback = Arc<dyn Fn(u64, Option<u64>) + Send + Sync>;

const DOWNLOAD_TIMEOUT_SECS: u64 = 300;
const CONNECT_TIMEOUT_SECS: u64 = 30;
const USER_AGENT_STRING: &str = "sps package manager (Rust; +https://github.com/alexykn/sp)";

pub async fn fetch_formula_source_or_bottle(
    formula_name: &str,
    url: &str,
    sha256_expected: &str,
    mirrors: &[String],
    config: &Config,
) -> Result<PathBuf> {
    fetch_formula_source_or_bottle_with_progress(
        formula_name,
        url,
        sha256_expected,
        mirrors,
        config,
        None,
    )
    .await
}

pub async fn fetch_formula_source_or_bottle_with_progress(
    formula_name: &str,
    url: &str,
    sha256_expected: &str,
    mirrors: &[String],
    config: &Config,
    progress_callback: Option<ProgressCallback>,
) -> Result<PathBuf> {
    let filename = url
        .split('/')
        .next_back()
        .map(|s| s.to_string())
        .unwrap_or_else(|| format!("{formula_name}-download"));
    let cache_path = config.cache_dir().join(&filename);

    tracing::debug!(
        "Preparing to fetch main resource for '{}' from URL: {}",
        formula_name,
        url
    );
    tracing::debug!("Target cache path: {}", cache_path.display());
    tracing::debug!("Expected SHA256: {}", sha256_expected);

    if cache_path.is_file() {
        tracing::debug!("File exists in cache: {}", cache_path.display());
        if !sha256_expected.is_empty() {
            match verify_checksum(&cache_path, sha256_expected) {
                Ok(_) => {
                    tracing::debug!("Using valid cached file: {}", cache_path.display());
                    return Ok(cache_path);
                }
                Err(e) => {
                    debug!(
                        "Cached file checksum mismatch ({}): {}. Redownloading.",
                        cache_path.display(),
                        e
                    );
                    if let Err(remove_err) = fs::remove_file(&cache_path) {
                        debug!(
                            "Failed to remove corrupted cached file {}: {}",
                            cache_path.display(),
                            remove_err
                        );
                    }
                }
            }
        } else {
            tracing::debug!(
                "Using cached file (no checksum provided): {}",
                cache_path.display()
            );
            return Ok(cache_path);
        }
    } else {
        tracing::debug!("File not found in cache.");
    }

    fs::create_dir_all(config.cache_dir()).map_err(|e| {
        SpsError::IoError(format!(
            "Failed to create cache directory {}: {}",
            config.cache_dir().display(),
            e
        ))
    })?;
    // Validate primary URL
    validate_url(url)?;

    let client = build_http_client()?;

    let urls_to_try = std::iter::once(url).chain(mirrors.iter().map(|s| s.as_str()));
    let mut last_error: Option<SpsError> = None;

    for current_url in urls_to_try {
        // Validate mirror URL
        validate_url(current_url)?;
        tracing::debug!("Attempting download from: {}", current_url);
        match download_and_verify(
            &client,
            current_url,
            &cache_path,
            sha256_expected,
            progress_callback.clone(),
        )
        .await
        {
            Ok(path) => {
                tracing::debug!("Successfully downloaded and verified: {}", path.display());
                return Ok(path);
            }
            Err(e) => {
                error!("Download attempt failed from {}: {}", current_url, e);
                last_error = Some(e);
            }
        }
    }

    Err(last_error.unwrap_or_else(|| {
        SpsError::DownloadError(
            formula_name.to_string(),
            url.to_string(),
            "All download attempts failed.".to_string(),
        )
    }))
}

pub async fn fetch_resource(
    formula_name: &str,
    resource: &ResourceSpec,
    config: &Config,
) -> Result<PathBuf> {
    fetch_resource_with_progress(formula_name, resource, config, None).await
}

pub async fn fetch_resource_with_progress(
    formula_name: &str,
    resource: &ResourceSpec,
    config: &Config,
    progress_callback: Option<ProgressCallback>,
) -> Result<PathBuf> {
    let resource_cache_dir = config.cache_dir().join("resources");
    fs::create_dir_all(&resource_cache_dir).map_err(|e| {
        SpsError::IoError(format!(
            "Failed to create resource cache directory {}: {}",
            resource_cache_dir.display(),
            e
        ))
    })?;
    // Validate resource URL
    validate_url(&resource.url)?;

    let url_filename = resource
        .url
        .split('/')
        .next_back()
        .map(|s| s.to_string())
        .unwrap_or_else(|| format!("{}-download", resource.name));
    let cache_filename = format!("{}-{}", resource.name, url_filename);
    let cache_path = resource_cache_dir.join(&cache_filename);

    tracing::debug!(
        "Preparing to fetch resource '{}' for formula '{}' from URL: {}",
        resource.name,
        formula_name,
        resource.url
    );
    tracing::debug!("Target resource cache path: {}", cache_path.display());
    tracing::debug!("Expected SHA256: {}", resource.sha256);

    if cache_path.is_file() {
        tracing::debug!("Resource exists in cache: {}", cache_path.display());
        match verify_checksum(&cache_path, &resource.sha256) {
            Ok(_) => {
                tracing::debug!("Using cached resource: {}", cache_path.display());
                return Ok(cache_path);
            }
            Err(e) => {
                debug!(
                    "Cached resource checksum mismatch ({}): {}. Redownloading.",
                    cache_path.display(),
                    e
                );
                if let Err(remove_err) = fs::remove_file(&cache_path) {
                    debug!(
                        "Failed to remove corrupted cached resource file {}: {}",
                        cache_path.display(),
                        remove_err
                    );
                }
            }
        }
    } else {
        tracing::debug!("Resource not found in cache.");
    }

    let client = build_http_client()?;
    match download_and_verify(
        &client,
        &resource.url,
        &cache_path,
        &resource.sha256,
        progress_callback,
    )
    .await
    {
        Ok(path) => {
            tracing::debug!(
                "Successfully downloaded and verified resource: {}",
                path.display()
            );
            Ok(path)
        }
        Err(e) => {
            error!("Resource download failed from {}: {}", resource.url, e);
            let _ = fs::remove_file(&cache_path);
            Err(SpsError::DownloadError(
                resource.name.clone(),
                resource.url.clone(),
                format!("Download failed: {e}"),
            ))
        }
    }
}

fn build_http_client() -> Result<Client> {
    let mut headers = HeaderMap::new();
    headers.insert(USER_AGENT, USER_AGENT_STRING.parse().unwrap());
    headers.insert(ACCEPT, "*/*".parse().unwrap());
    Client::builder()
        .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT_SECS))
        .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
        .default_headers(headers)
        .redirect(reqwest::redirect::Policy::limited(10))
        .build()
        .map_err(|e| SpsError::HttpError(format!("Failed to build HTTP client: {e}")))
}

async fn download_and_verify(
    client: &Client,
    url: &str,
    final_path: &Path,
    sha256_expected: &str,
    progress_callback: Option<ProgressCallback>,
) -> Result<PathBuf> {
    let temp_filename = format!(
        ".{}.download",
        final_path.file_name().unwrap_or_default().to_string_lossy()
    );
    let temp_path = final_path.with_file_name(temp_filename);
    tracing::debug!("Downloading to temporary path: {}", temp_path.display());
    if temp_path.exists() {
        if let Err(e) = fs::remove_file(&temp_path) {
            tracing::warn!(
                "Could not remove existing temporary file {}: {}",
                temp_path.display(),
                e
            );
        }
    }

    let response = client.get(url).send().await.map_err(|e| {
        debug!("HTTP request failed for {url}: {e}");
        SpsError::HttpError(format!("HTTP request failed for {url}: {e}"))
    })?;
    let status = response.status();
    tracing::debug!("Received HTTP status: {} for {}", status, url);

    if !status.is_success() {
        let body_text = response
            .text()
            .await
            .unwrap_or_else(|_| "Failed to read response body".to_string());
        tracing::error!("HTTP error {} for URL {}: {}", status, url, body_text);
        return match status {
            StatusCode::NOT_FOUND => Err(SpsError::DownloadError(
                final_path
                    .file_name()
                    .map(|s| s.to_string_lossy().to_string())
                    .unwrap_or_default(),
                url.to_string(),
                "Resource not found (404)".to_string(),
            )),
            StatusCode::FORBIDDEN => Err(SpsError::DownloadError(
                final_path
                    .file_name()
                    .map(|s| s.to_string_lossy().to_string())
                    .unwrap_or_default(),
                url.to_string(),
                "Access forbidden (403)".to_string(),
            )),
            _ => Err(SpsError::HttpError(format!(
                "HTTP error {status} for URL {url}: {body_text}"
            ))),
        };
    }

    // Get total size from Content-Length header if available
    let total_size = response.content_length();

    let mut temp_file = TokioFile::create(&temp_path).await.map_err(|e| {
        SpsError::IoError(format!(
            "Failed to create temp file {}: {}",
            temp_path.display(),
            e
        ))
    })?;

    // Use bytes_stream() for chunked download with progress reporting
    let mut stream = response.bytes_stream();
    let mut bytes_downloaded = 0u64;

    while let Some(chunk) = stream.next().await {
        let chunk = chunk.map_err(|e| SpsError::HttpError(format!("Failed to read chunk: {e}")))?;

        temp_file.write_all(&chunk).await.map_err(|e| {
            SpsError::IoError(format!(
                "Failed to write chunk to {}: {}",
                temp_path.display(),
                e
            ))
        })?;

        bytes_downloaded += chunk.len() as u64;

        // Call progress callback if provided
        if let Some(ref callback) = progress_callback {
            callback(bytes_downloaded, total_size);
        }
    }

    drop(temp_file);
    tracing::debug!("Finished writing download stream to temp file.");

    if !sha256_expected.is_empty() {
        verify_checksum(&temp_path, sha256_expected)?;
        tracing::debug!(
            "Checksum verified for temporary file: {}",
            temp_path.display()
        );
    } else {
        tracing::warn!(
            "Skipping checksum verification for {} - none provided.",
            temp_path.display()
        );
    }

    fs::rename(&temp_path, final_path).map_err(|e| {
        SpsError::IoError(format!(
            "Failed to move temp file {} to {}: {}",
            temp_path.display(),
            final_path.display(),
            e
        ))
    })?;
    tracing::debug!(
        "Moved verified file to final location: {}",
        final_path.display()
    );
    Ok(final_path.to_path_buf())
}

use std::sync::Arc;

use clap::Args;
use colored::Colorize;
use prettytable::{format, Cell, Row, Table};
use serde_json::Value;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result;
use sps_net::api;
use terminal_size::{terminal_size, Width};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

#[derive(Args, Debug)]
pub struct Search {
    pub query: String,
    #[arg(long, conflicts_with = "cask")]
    pub formula: bool,
    #[arg(long, conflicts_with = "formula")]
    pub cask: bool,
}

pub enum SearchType {
    All,
    Formula,
    Cask,
}

impl Search {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        let search_type = if self.formula {
            SearchType::Formula
        } else if self.cask {
            SearchType::Cask
        } else {
            SearchType::All
        };
        run_search(&self.query, search_type, config, cache).await
    }
}

pub async fn run_search(
    query: &str,
    search_type: SearchType,
    _config: &Config,
    cache: Arc<Cache>,
) -> Result<()> {
    tracing::debug!("Searching for packages matching: {}", query);

    println!("Searching for \"{query}\"");

    let mut formula_matches = Vec::new();
    let mut cask_matches = Vec::new();
    let mut formula_err = None;
    let mut cask_err = None;

    if matches!(search_type, SearchType::All | SearchType::Formula) {
        match search_formulas(Arc::clone(&cache), query).await {
            Ok(matches) => formula_matches = matches,
            Err(e) => {
                tracing::error!("Error searching formulas: {}", e);
                formula_err = Some(e);
            }
        }
    }

    if matches!(search_type, SearchType::All | SearchType::Cask) {
        match search_casks(Arc::clone(&cache), query).await {
            Ok(matches) => cask_matches = matches,
            Err(e) => {
                tracing::error!("Error searching casks: {}", e);
                cask_err = Some(e);
            }
        }
    }

    if formula_matches.is_empty() && cask_matches.is_empty() {
        if let Some(e) = formula_err.or(cask_err) {
            return Err(e);
        }
    }

    print_search_results(query, &formula_matches, &cask_matches);

    Ok(())
}

async fn search_formulas(cache: Arc<Cache>, query: &str) -> Result<Vec<Value>> {
    let query_lower = query.to_lowercase();
    let mut matches = Vec::new();
    let mut data_source_name = "cache";

    let formula_data_result = cache.load_raw("formula.json");

    let formulas: Vec<Value> = match formula_data_result {
        Ok(formula_data) => serde_json::from_str(&formula_data)?,
        Err(e) => {
            tracing::debug!("Formula cache load failed ({}), fetching from API", e);
            data_source_name = "API";
            let all_formulas = api::fetch_all_formulas().await?;

            if let Err(cache_err) = cache.store_raw("formula.json", &all_formulas) {
                tracing::warn!("Failed to cache formula data after fetching: {}", cache_err);
            }
            serde_json::from_str(&all_formulas)?
        }
    };

    for formula in formulas {
        if is_formula_match(&formula, &query_lower) {
            matches.push(formula);
        }
    }

    tracing::debug!(
        "Found {} potential formula matches from {}",
        matches.len(),
        data_source_name
    );
    tracing::debug!(
        "Filtered down to {} formula matches with available bottles",
        matches.len()
    );

    Ok(matches)
}

async fn search_casks(cache: Arc<Cache>, query: &str) -> Result<Vec<Value>> {
    let query_lower = query.to_lowercase();
    let mut matches = Vec::new();
    let mut data_source_name = "cache";

    let cask_data_result = cache.load_raw("cask.json");

    let casks: Vec<Value> = match cask_data_result {
        Ok(cask_data) => serde_json::from_str(&cask_data)?,
        Err(e) => {
            tracing::debug!("Cask cache load failed ({}), fetching from API", e);
            data_source_name = "API";
            let all_casks = api::fetch_all_casks().await?;

            if let Err(cache_err) = cache.store_raw("cask.json", &all_casks) {
                tracing::warn!("Failed to cache cask data after fetching: {}", cache_err);
            }
            serde_json::from_str(&all_casks)?
        }
    };

    for cask in casks {
        if is_cask_match(&cask, &query_lower) {
            matches.push(cask);
        }
    }
    tracing::debug!(
        "Found {} cask matches from {}",
        matches.len(),
        data_source_name
    );
    Ok(matches)
}

fn is_formula_match(formula: &Value, query: &str) -> bool {
    if let Some(name) = formula.get("name").and_then(|n| n.as_str()) {
        if name.to_lowercase().contains(query) {
            return true;
        }
    }

    if let Some(full_name) = formula.get("full_name").and_then(|n| n.as_str()) {
        if full_name.to_lowercase().contains(query) {
            return true;
        }
    }

    if let Some(desc) = formula.get("desc").and_then(|d| d.as_str()) {
        if desc.to_lowercase().contains(query) {
            return true;
        }
    }

    if let Some(aliases) = formula.get("aliases").and_then(|a| a.as_array()) {
        for alias in aliases {
            if let Some(alias_str) = alias.as_str() {
                if alias_str.to_lowercase().contains(query) {
                    return true;
                }
            }
        }
    }

    false
}

fn is_cask_match(cask: &Value, query: &str) -> bool {
    if let Some(token) = cask.get("token").and_then(|t| t.as_str()) {
        if token.to_lowercase().contains(query) {
            return true;
        }
    }

    if let Some(names) = cask.get("name").and_then(|n| n.as_array()) {
        for name in names {
            if let Some(name_str) = name.as_str() {
                if name_str.to_lowercase().contains(query) {
                    return true;
                }
            }
        }
    }

    if let Some(desc) = cask.get("desc").and_then(|d| d.as_str()) {
        if desc.to_lowercase().contains(query) {
            return true;
        }
    }

    false
}

fn truncate_vis(s: &str, max: usize) -> String {
    if UnicodeWidthStr::width(s) <= max {
        return s.to_string();
    }
    let mut w = 0;
    let mut out = String::new();
    let effective_max = if max > 0 { max } else { 1 };

    for ch in s.chars() {
        let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
        if w + cw >= effective_max.saturating_sub(1) {
            break;
        }
        out.push(ch);
        w += cw;
    }
    out.push('…');
    out
}

pub fn print_search_results(query: &str, formula_matches: &[Value], cask_matches: &[Value]) {
    let total = formula_matches.len() + cask_matches.len();
    if total == 0 {
        println!("{}", format!("No matches found for '{query}'").yellow());
        return;
    }
    println!(
        "{}",
        format!("Found {total} result(s) for '{query}'").bold()
    );

    let term_cols = terminal_size()
        .map(|(Width(w), _)| w as usize)
        .unwrap_or(120);

    let type_col = 7;
    let version_col = 10;
    let sep_width = 3 * 3;
    let total_fixed = type_col + version_col + sep_width;

    let name_min_width = 10;
    let desc_min_width = 20;

    let leftover = term_cols.saturating_sub(total_fixed);

    let name_prop_width = leftover / 3;

    let name_max = std::cmp::max(name_min_width, name_prop_width);
    let desc_max = std::cmp::max(desc_min_width, leftover.saturating_sub(name_max));

    let name_max = std::cmp::min(name_max, leftover.saturating_sub(desc_min_width));
    let desc_max = std::cmp::min(desc_max, leftover.saturating_sub(name_max));

    let mut tbl = Table::new();
    tbl.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);

    for formula in formula_matches {
        let raw_name = formula
            .get("name")
            .and_then(|n| n.as_str())
            .unwrap_or("Unknown");
        let raw_desc = formula.get("desc").and_then(|d| d.as_str()).unwrap_or("");
        let _name = truncate_vis(raw_name, name_max);
        let desc = truncate_vis(raw_desc, desc_max);

        let version = get_version(formula);

        tbl.add_row(Row::new(vec![
            Cell::new("Formula").style_spec("Fg"),
            Cell::new(&_name).style_spec("Fb"),
            Cell::new(version),
            Cell::new(&desc),
        ]));
    }

    if !formula_matches.is_empty() && !cask_matches.is_empty() {
        tbl.add_row(Row::new(vec![Cell::new(" ").with_hspan(4)]));
    }

    for cask in cask_matches {
        let raw_name = cask
            .get("token")
            .and_then(|t| t.as_str())
            .unwrap_or("Unknown");
        let raw_desc = cask.get("desc").and_then(|d| d.as_str()).unwrap_or("");
        let desc = truncate_vis(raw_desc, desc_max);

        let version = get_cask_version(cask);

        tbl.add_row(Row::new(vec![
            Cell::new("Cask").style_spec("Fy"),
            Cell::new(raw_name).style_spec("Fb"),
            Cell::new(version),
            Cell::new(&desc),
        ]));
    }

    tbl.printstd();
}

fn get_version(formula: &Value) -> &str {
    formula
        .get("versions")
        .and_then(|v| v.get("stable"))
        .and_then(|v| v.as_str())
        .unwrap_or("-")
}

fn get_cask_version(cask: &Value) -> &str {
    cask.get("version").and_then(|v| v.as_str()).unwrap_or("-")
}

// sps/src/main.rs
use std::process::{self}; // StdCommand is used
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use std::{env, fs};

use clap::Parser;
use colored::Colorize;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::{Result as spResult, SpsError};
use tracing::level_filters::LevelFilter;
use tracing::{debug, error, warn}; // Import all necessary tracing macros
use tracing_subscriber::fmt::writer::MakeWriterExt;
use tracing_subscriber::EnvFilter;

mod cli;
mod pipeline;
// Correctly import InitArgs via the re-export in cli.rs or directly from its module
use cli::{CliArgs, Command, InitArgs};

// Standalone function to handle the init command logic
async fn run_init_command(init_args: &InitArgs, verbose_level: u8) -> spResult<()> {
    let init_level_filter = match verbose_level {
        0 => LevelFilter::INFO,
        1 => LevelFilter::DEBUG,
        _ => LevelFilter::TRACE,
    };
    let _ = tracing_subscriber::fmt()
        .with_max_level(init_level_filter)
        .with_writer(std::io::stderr)
        .with_ansi(true)
        .without_time()
        .try_init();

    let initial_config_for_path = Config::load().map_err(|e| {
        // Handle error if even basic config loading fails for path determination
        SpsError::Config(format!(
            "Could not determine sps_root for init (config load failed): {e}"
        ))
    })?;

    // Create a minimal Config struct, primarily for sps_root() and derived paths.
    let temp_config_for_init = Config {
        sps_root: initial_config_for_path.sps_root().to_path_buf(),
        api_base_url: "https://formulae.brew.sh/api".to_string(),
        artifact_domain: None,
        docker_registry_token: None,
        docker_registry_basic_auth: None,
        github_api_token: None,
    };

    init_args.run(&temp_config_for_init).await
}

#[tokio::main]
async fn main() -> spResult<()> {
    let cli_args = CliArgs::parse();

    if let Command::Init(ref init_args_ref) = cli_args.command {
        match run_init_command(init_args_ref, cli_args.verbose).await {
            Ok(_) => {
                return Ok(());
            }
            Err(e) => {
                eprintln!("{}: Init command failed: {:#}", "Error".red().bold(), e);
                process::exit(1);
            }
        }
    }

    let config = Config::load().map_err(|e| {
        SpsError::Config(format!(
            "Could not load config (have you run 'sps init'?): {e}"
        ))
    })?;

    let level_filter = match cli_args.verbose {
        0 => LevelFilter::INFO,
        1 => LevelFilter::DEBUG,
        _ => LevelFilter::TRACE,
    };
    let max_log_level = level_filter.into_level().unwrap_or(tracing::Level::INFO);

    let env_filter = EnvFilter::builder()
        .with_default_directive(level_filter.into())
        .with_env_var("SPS_LOG")
        .from_env_lossy();

    let log_dir = config.logs_dir();
    if let Err(e) = fs::create_dir_all(&log_dir) {
        eprintln!(
            "{} Failed to create log directory {}: {} (ensure 'sps init' was successful or try with sudo for the current command if appropriate)",
            "Error:".red().bold(),
            log_dir.display(),
            e
        );
        let _ = tracing_subscriber::fmt() // Use `let _ =`
            .with_env_filter(env_filter)
            .with_writer(std::io::stderr)
            .with_ansi(true)
            .without_time()
            .try_init(); // Use try_init
    } else if cli_args.verbose > 0 {
        let file_appender = tracing_appender::rolling::daily(&log_dir, "sps.log");
        let (non_blocking_appender, guard) = tracing_appender::non_blocking(file_appender);

        // For verbose mode, show debug/trace logs on stderr too
        let stderr_writer = std::io::stderr.with_max_level(max_log_level);
        let file_writer = non_blocking_appender.with_max_level(max_log_level);

        let _ = tracing_subscriber::fmt() // Use `let _ =`
            .with_env_filter(env_filter)
            .with_writer(stderr_writer.and(file_writer))
            .with_ansi(true)
            .without_time()
            .try_init(); // Use try_init

        Box::leak(Box::new(guard)); // Keep guard alive

        tracing::debug!(
            // This will only work if try_init above was successful for this setup
            "Verbose logging enabled. Writing logs to: {}/sps.log",
            log_dir.display()
        );
    } else {
        let _ = tracing_subscriber::fmt() // Use `let _ =`
            .with_env_filter(env_filter)
            .with_writer(std::io::stderr)
            .with_ansi(true)
            .without_time()
            .try_init(); // Use try_init
    }

    let cache = Arc::new(Cache::new(&config).map_err(|e| {
        SpsError::Cache(format!(
            "Could not initialize cache (ensure 'sps init' was successful): {e}"
        ))
    })?);

    let needs_update_check = matches!(
        cli_args.command,
        Command::Install(_) | Command::Search { .. } | Command::Info { .. } | Command::Upgrade(_)
    );

    if needs_update_check {
        if let Err(e) = check_and_run_auto_update(&config, Arc::clone(&cache)).await {
            error!("Error during auto-update check: {}", e); // Use `error!` macro
        }
    } else {
        debug!(
            // Use `debug!` macro
            "Skipping auto-update check for command: {:?}",
            cli_args.command
        );
    }

    // Pass config and cache to the command's run method
    let command_execution_result = match &cli_args.command {
        Command::Init(_) => {
            /* This case is handled above and main exits */
            unreachable!()
        }
        _ => cli_args.command.run(&config, cache).await,
    };

    if let Err(e) = command_execution_result {
        // For pipeline commands (Install, Reinstall, Upgrade), errors are already
        // displayed via the status system, so only log in verbose mode
        let is_pipeline_command = matches!(
            cli_args.command,
            Command::Install(_) | Command::Reinstall(_) | Command::Upgrade(_)
        );

        if is_pipeline_command {
            // Only show error details in verbose mode
            if cli_args.verbose > 0 {
                error!("Command failed: {:#}", e);
                eprintln!("{}: {:#}", "Error".red().bold(), e);
            }
        } else {
            // For non-pipeline commands, show errors normally
            error!("Command failed: {:#}", e);
            eprintln!("{}: {:#}", "Error".red().bold(), e);
        }
        process::exit(1);
    }

    debug!("Command completed successfully."); // Use `debug!` macro
    Ok(())
}

async fn check_and_run_auto_update(config: &Config, cache: Arc<Cache>) -> spResult<()> {
    if env::var("SPS_NO_AUTO_UPDATE").is_ok_and(|v| v == "1") {
        debug!("Auto-update disabled via SPS_NO_AUTO_UPDATE=1.");
        return Ok(());
    }

    let default_interval_secs: u64 = 86400;
    let update_interval_secs = env::var("SPS_AUTO_UPDATE_SECS")
        .ok()
        .and_then(|s| s.parse::<u64>().ok())
        .unwrap_or(default_interval_secs);
    let update_interval = Duration::from_secs(update_interval_secs);
    debug!("Auto-update interval: {:?}", update_interval);

    let timestamp_file = config.state_dir().join(".sps_last_update_check");
    debug!("Checking timestamp file: {}", timestamp_file.display());

    if let Some(parent_dir) = timestamp_file.parent() {
        if !parent_dir.exists() {
            if let Err(e) = fs::create_dir_all(parent_dir) {
                warn!("Could not create state directory {} as user: {}. Auto-update might rely on 'sps init' to create this with sudo.", parent_dir.display(), e);
            }
        }
    }

    let mut needs_update = true;
    if timestamp_file.exists() {
        if let Ok(metadata) = fs::metadata(&timestamp_file) {
            if let Ok(modified_time) = metadata.modified() {
                match SystemTime::now().duration_since(modified_time) {
                    Ok(age) => {
                        debug!("Time since last update check: {:?}", age);
                        if age < update_interval {
                            needs_update = false;
                            debug!("Auto-update interval not yet passed.");
                        } else {
                            debug!("Auto-update interval passed.");
                        }
                    }
                    Err(e) => {
                        warn!(
                            "Could not get duration since last update check (system time error?): {}",
                            e
                        );
                    }
                }
            } else {
                warn!(
                    "Could not read modification time for timestamp file: {}",
                    timestamp_file.display()
                );
            }
        } else {
            debug!(
                "Timestamp file {} metadata could not be read. Update needed.",
                timestamp_file.display()
            );
        }
    } else {
        debug!(
            "Timestamp file not found at {}. Update needed.",
            timestamp_file.display()
        );
    }

    if needs_update {
        println!(
            "{}{}",
            "==> ".bold().blue(),
            "Running auto-update...".bold()
        );
        match cli::update::Update.run(config, cache).await {
            Ok(_) => {
                println!(
                    "{}{}",
                    "==> ".bold().blue(),
                    "Auto-update successful.".bold()
                );
                match fs::File::create(&timestamp_file) {
                    Ok(_) => {
                        debug!("Updated timestamp file: {}", timestamp_file.display());
                    }
                    Err(e) => {
                        warn!(
                            "Failed to create or update timestamp file '{}': {}",
                            timestamp_file.display(),
                            e
                        );
                    }
                }
            }
            Err(e) => {
                error!("Auto-update failed: {}", e);
                eprintln!("{} Auto-update failed: {}", "Warning:".yellow(), e);
            }
        }
    } else {
        debug!("Skipping auto-update.");
    }

    Ok(())
}

// In sps-core/src/build/cask/dmg.rs

use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::Command;

use sps_common::error::{Result, SpsError};
use tracing::{debug, error}; // Added log imports

// --- Keep Existing Helpers ---
pub fn mount_dmg(dmg_path: &Path) -> Result<PathBuf> {
    debug!("Mounting DMG: {}", dmg_path.display());
    let output = Command::new("hdiutil")
        .arg("attach")
        .arg("-plist")
        .arg("-nobrowse")
        .arg("-readonly")
        .arg("-mountrandom")
        .arg("/tmp") // Consider making mount location configurable or more robust
        .arg(dmg_path)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        error!(
            "hdiutil attach failed for {}: {}",
            dmg_path.display(),
            stderr
        );
        return Err(SpsError::Generic(format!(
            "Failed to mount DMG '{}': {}",
            dmg_path.display(),
            stderr
        )));
    }

    let mount_point = parse_mount_point(&output.stdout)?;
    debug!("DMG mounted at: {}", mount_point.display());
    Ok(mount_point)
}

pub fn unmount_dmg(mount_point: &Path) -> Result<()> {
    debug!("Unmounting DMG from: {}", mount_point.display());
    // Add logging for commands
    debug!("Executing: hdiutil detach -force {}", mount_point.display());
    let output = Command::new("hdiutil")
        .arg("detach")
        .arg("-force")
        .arg(mount_point)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        debug!(
            "hdiutil detach failed ({}): {}. Trying diskutil",
            output.status, stderr
        );
        // Add logging for fallback
        debug!(
            "Executing: diskutil unmount force {}",
            mount_point.display()
        );
        let diskutil_output = Command::new("diskutil")
            .arg("unmount")
            .arg("force")
            .arg(mount_point)
            .output()?;

        if !diskutil_output.status.success() {
            let diskutil_stderr = String::from_utf8_lossy(&diskutil_output.stderr);
            error!(
                "diskutil unmount force failed ({}): {}",
                diskutil_output.status, diskutil_stderr
            );
            // Consider returning error only if both fail? Or always error on diskutil fail?
            return Err(SpsError::Generic(format!(
                "Failed to unmount DMG '{}' using hdiutil and diskutil: {}",
                mount_point.display(),
                diskutil_stderr
            )));
        }
    }
    debug!("DMG successfully unmounted");
    Ok(())
}

fn parse_mount_point(output: &[u8]) -> Result<PathBuf> {
    // ... (existing implementation) ...
    // Use plist crate for more robust parsing if possible in the future
    let cursor = std::io::Cursor::new(output);
    let reader = BufReader::new(cursor);
    let mut in_sys_entities = false;
    let mut in_mount_point = false;
    let mut mount_path_str: Option<String> = None;

    for line_res in reader.lines() {
        let line = line_res?;
        let trimmed = line.trim();

        if trimmed == "<key>system-entities</key>" {
            in_sys_entities = true;
            continue;
        }
        if !in_sys_entities {
            continue;
        }

        if trimmed == "<key>mount-point</key>" {
            in_mount_point = true;
            continue;
        }

        if in_mount_point && trimmed.starts_with("<string>") && trimmed.ends_with("</string>") {
            mount_path_str = Some(
                trimmed
                    .trim_start_matches("<string>")
                    .trim_end_matches("</string>")
                    .to_string(),
            );
            break; // Found the first mount point, assume it's the main one
        }

        // Reset flags if we encounter closing tags for structures containing mount-point
        if trimmed == "</dict>" {
            in_mount_point = false;
        }
        if trimmed == "</array>" && in_sys_entities {
            // End of system-entities
            // break; // Stop searching if we leave the system-entities array
            in_sys_entities = false; // Reset this flag too
        }
    }

    match mount_path_str {
        Some(path_str) if !path_str.is_empty() => {
            debug!("Parsed mount point from plist: {}", path_str);
            Ok(PathBuf::from(path_str))
        }
        _ => {
            error!("Failed to parse mount point from hdiutil plist output.");
            // Optionally log the raw output for debugging
            // error!("Raw hdiutil output:\n{}", String::from_utf8_lossy(output));
            Err(SpsError::Generic(
                "Failed to determine mount point from hdiutil output".to_string(),
            ))
        }
    }
}

// --- NEW Function ---
/// Extracts the contents of a mounted DMG to a staging directory using `ditto`.
pub fn extract_dmg_to_stage(dmg_path: &Path, stage_dir: &Path) -> Result<()> {
    let mount_point = mount_dmg(dmg_path)?;

    // Ensure the stage directory exists (though TempDir should handle it)
    if !stage_dir.exists() {
        fs::create_dir_all(stage_dir).map_err(|e| SpsError::Io(std::sync::Arc::new(e)))?;
    }

    debug!(
        "Copying contents from DMG mount {} to stage {} using ditto",
        mount_point.display(),
        stage_dir.display()
    );
    // Use ditto for robust copying, preserving metadata
    // ditto <source> <destination>
    debug!(
        "Executing: ditto {} {}",
        mount_point.display(),
        stage_dir.display()
    );
    let ditto_output = Command::new("ditto")
        .arg(&mount_point) // Source first
        .arg(stage_dir) // Then destination
        .output()?;

    let unmount_result = unmount_dmg(&mount_point); // Unmount regardless of ditto success

    if !ditto_output.status.success() {
        let stderr = String::from_utf8_lossy(&ditto_output.stderr);
        error!("ditto command failed ({}): {}", ditto_output.status, stderr);
        // Also log stdout which might contain info on specific file errors
        let stdout = String::from_utf8_lossy(&ditto_output.stdout);
        if !stdout.trim().is_empty() {
            error!("ditto stdout: {}", stdout);
        }
        unmount_result?; // Ensure we still return unmount error if it happened
        return Err(SpsError::Generic(format!(
            "Failed to copy DMG contents using ditto: {stderr}"
        )));
    }

    // After ditto, quarantine any .app bundles in the stage (macOS only)
    #[cfg(target_os = "macos")]
    {
        if let Err(e) = crate::install::extract::quarantine_extracted_apps_in_stage(
            stage_dir,
            "sps-dmg-extractor",
        ) {
            tracing::warn!(
                "Error during post-DMG extraction quarantine scan for {}: {}",
                dmg_path.display(),
                e
            );
        }
    }

    unmount_result // Return the result of unmounting
}

use std::env;
use std::path::PathBuf;
use std::process::{Command, Stdio};

use sps_common::error::{Result, SpsError};
use tracing::debug;
use which;

pub fn find_compiler(name: &str) -> Result<PathBuf> {
    let env_var_name = match name {
        "cc" => "CC",
        "c++" | "cxx" => "CXX",
        _ => "",
    };
    if !env_var_name.is_empty() {
        if let Ok(compiler_path) = env::var(env_var_name) {
            let path = PathBuf::from(compiler_path);
            if path.is_file() {
                debug!(
                    "Using compiler from env var {}: {}",
                    env_var_name,
                    path.display()
                );
                return Ok(path);
            } else {
                debug!(
                    "Env var {} points to non-existent file: {}",
                    env_var_name,
                    path.display()
                );
            }
        }
    }

    if cfg!(target_os = "macos") {
        debug!("Attempting to find '{name}' using xcrun");
        let output = Command::new("xcrun")
            .arg("--find")
            .arg(name)
            .stderr(Stdio::piped())
            .output();

        match output {
            Ok(out) if out.status.success() => {
                let path_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
                if !path_str.is_empty() {
                    let path = PathBuf::from(path_str);
                    if path.is_file() {
                        debug!("Found compiler via xcrun: {}", path.display());
                        return Ok(path);
                    } else {
                        debug!(
                            "xcrun found '{}' but path doesn't exist or isn't a file: {}",
                            name,
                            path.display()
                        );
                    }
                } else {
                    debug!("xcrun found '{name}' but returned empty path.");
                }
            }
            Ok(out) => {
                let stderr = String::from_utf8_lossy(&out.stderr);
                debug!("xcrun failed to find '{}': {}", name, stderr.trim());
            }
            Err(e) => {
                debug!("Failed to execute xcrun: {e}. Falling back to PATH search.");
            }
        }
    }

    debug!("Falling back to searching PATH for '{name}'");
    which::which(name).map_err(|e| {
        SpsError::BuildEnvError(format!("Failed to find compiler '{name}' on PATH: {e}"))
    })
}

pub fn find_sdk_path() -> Result<PathBuf> {
    if cfg!(target_os = "macos") {
        debug!("Attempting to find macOS SDK path using xcrun");
        let output = Command::new("xcrun")
            .arg("--show-sdk-path")
            .stderr(Stdio::piped())
            .output();

        match output {
            Ok(out) if out.status.success() => {
                let path_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
                if path_str.is_empty() || path_str == "/" {
                    return Err(SpsError::BuildEnvError(
                        "xcrun returned empty or invalid SDK path. Is Xcode or Command Line Tools installed correctly?".to_string()
                    ));
                }
                let sdk_path = PathBuf::from(path_str);
                if !sdk_path.exists() {
                    return Err(SpsError::BuildEnvError(format!(
                        "SDK path reported by xcrun does not exist: {}",
                        sdk_path.display()
                    )));
                }
                debug!("Found SDK path: {}", sdk_path.display());
                Ok(sdk_path)
            }
            Ok(out) => {
                let stderr = String::from_utf8_lossy(&out.stderr);
                Err(SpsError::BuildEnvError(format!(
                    "xcrun failed to find SDK path: {}",
                    stderr.trim()
                )))
            }
            Err(e) => {
                Err(SpsError::BuildEnvError(format!(
                    "Failed to execute 'xcrun --show-sdk-path': {e}. Is Xcode or Command Line Tools installed?"
                )))
            }
        }
    } else {
        debug!("Not on macOS, returning '/' as SDK path placeholder");
        Ok(PathBuf::from("/"))
    }
}

pub fn get_macos_version() -> Result<String> {
    if cfg!(target_os = "macos") {
        debug!("Attempting to get macOS version using sw_vers");
        let output = Command::new("sw_vers")
            .arg("-productVersion")
            .stderr(Stdio::piped())
            .output();

        match output {
            Ok(out) if out.status.success() => {
                let version_full = String::from_utf8_lossy(&out.stdout).trim().to_string();
                let version_parts: Vec<&str> = version_full.split('.').collect();
                let version_short = if version_parts.len() >= 2 {
                    format!("{}.{}", version_parts[0], version_parts[1])
                } else {
                    version_full.clone()
                };
                debug!("Found macOS version: {version_full} (short: {version_short})");
                Ok(version_short)
            }
            Ok(out) => {
                let stderr = String::from_utf8_lossy(&out.stderr);
                Err(SpsError::BuildEnvError(format!(
                    "sw_vers failed to get product version: {}",
                    stderr.trim()
                )))
            }
            Err(e) => Err(SpsError::BuildEnvError(format!(
                "Failed to execute 'sw_vers -productVersion': {e}"
            ))),
        }
    } else {
        debug!("Not on macOS, returning '0.0' as version placeholder");
        Ok(String::from("0.0"))
    }
}

pub fn get_arch_flag() -> String {
    if cfg!(target_os = "macos") {
        if cfg!(target_arch = "x86_64") {
            debug!("Detected target arch: x86_64");
            "-arch x86_64".to_string()
        } else if cfg!(target_arch = "aarch64") {
            debug!("Detected target arch: aarch64 (arm64)");
            "-arch arm64".to_string()
        } else {
            let arch = env::consts::ARCH;
            debug!(
                "Unknown target architecture on macOS: {arch}, cannot determine -arch flag. Build might fail."
            );
            // Provide no flag in this unknown case? Or default to native?
            String::new()
        }
    } else {
        debug!("Not on macOS, returning empty arch flag.");
        String::new()
    }
}

// sps-common/src/config.rs
use std::env;
use std::path::{Path, PathBuf};

use directories::UserDirs; // Ensure this crate is in sps-common/Cargo.toml
use tracing::debug;

use super::error::Result; // Assuming SpsResult is Result from super::error

// This constant will serve as a fallback if HOMEBREW_PREFIX is not set or is empty.
const DEFAULT_FALLBACK_SPS_ROOT: &str = "/opt/homebrew";
const SPS_ROOT_MARKER_FILENAME: &str = ".sps_root_v1";

#[derive(Debug, Clone)]
pub struct Config {
    pub sps_root: PathBuf, // Public for direct construction in main for init if needed
    pub api_base_url: String,
    pub artifact_domain: Option<String>,
    pub docker_registry_token: Option<String>,
    pub docker_registry_basic_auth: Option<String>,
    pub github_api_token: Option<String>,
}

impl Config {
    pub fn load() -> Result<Self> {
        debug!("Loading sps configuration");

        // Try to get SPS_ROOT from HOMEBREW_PREFIX environment variable.
        // Fallback to DEFAULT_FALLBACK_SPS_ROOT if not set or empty.
        let sps_root_str = env::var("HOMEBREW_PREFIX").ok().filter(|s| !s.is_empty())
            .unwrap_or_else(|| {
                debug!(
                    "HOMEBREW_PREFIX environment variable not set or empty, falling back to default: {}",
                    DEFAULT_FALLBACK_SPS_ROOT
                );
                DEFAULT_FALLBACK_SPS_ROOT.to_string()
            });

        let sps_root_path = PathBuf::from(&sps_root_str);
        debug!("Effective SPS_ROOT set to: {}", sps_root_path.display());

        let api_base_url = "https://formulae.brew.sh/api".to_string();

        let artifact_domain = env::var("HOMEBREW_ARTIFACT_DOMAIN").ok();
        let docker_registry_token = env::var("HOMEBREW_DOCKER_REGISTRY_TOKEN").ok();
        let docker_registry_basic_auth = env::var("HOMEBREW_DOCKER_REGISTRY_BASIC_AUTH_TOKEN").ok();
        let github_api_token = env::var("HOMEBREW_GITHUB_API_TOKEN").ok();

        debug!("Configuration loaded successfully.");
        Ok(Self {
            sps_root: sps_root_path,
            api_base_url,
            artifact_domain,
            docker_registry_token,
            docker_registry_basic_auth,
            github_api_token,
        })
    }

    pub fn sps_root(&self) -> &Path {
        &self.sps_root
    }

    pub fn bin_dir(&self) -> PathBuf {
        self.sps_root.join("bin")
    }

    pub fn cellar_dir(&self) -> PathBuf {
        self.sps_root.join("Cellar") // Changed from "cellar" to "Cellar" to match Homebrew
    }

    pub fn cask_room_dir(&self) -> PathBuf {
        self.sps_root.join("Caskroom") // Changed from "cask_room" to "Caskroom"
    }

    pub fn cask_store_dir(&self) -> PathBuf {
        self.sps_root.join("sps_cask_store")
    }

    pub fn opt_dir(&self) -> PathBuf {
        self.sps_root.join("opt")
    }

    pub fn taps_dir(&self) -> PathBuf {
        self.sps_root.join("Library/Taps") // Adjusted to match Homebrew structure
    }

    pub fn cache_dir(&self) -> PathBuf {
        self.sps_root.join("sps_cache")
    }

    pub fn logs_dir(&self) -> PathBuf {
        self.sps_root.join("sps_logs")
    }

    pub fn tmp_dir(&self) -> PathBuf {
        self.sps_root.join("tmp")
    }

    pub fn state_dir(&self) -> PathBuf {
        self.sps_root.join("sps_state")
    }

    pub fn man_base_dir(&self) -> PathBuf {
        self.sps_root.join("share").join("man")
    }

    pub fn sps_root_marker_path(&self) -> PathBuf {
        self.sps_root.join(SPS_ROOT_MARKER_FILENAME)
    }

    pub fn applications_dir(&self) -> PathBuf {
        if cfg!(target_os = "macos") {
            PathBuf::from("/Applications")
        } else {
            self.home_dir().join("Applications")
        }
    }

    pub fn formula_cellar_dir(&self, formula_name: &str) -> PathBuf {
        self.cellar_dir().join(formula_name)
    }

    pub fn formula_keg_path(&self, formula_name: &str, version_str: &str) -> PathBuf {
        self.formula_cellar_dir(formula_name).join(version_str)
    }

    pub fn formula_opt_path(&self, formula_name: &str) -> PathBuf {
        self.opt_dir().join(formula_name)
    }

    pub fn cask_room_token_path(&self, cask_token: &str) -> PathBuf {
        self.cask_room_dir().join(cask_token)
    }

    pub fn cask_store_token_path(&self, cask_token: &str) -> PathBuf {
        self.cask_store_dir().join(cask_token)
    }

    pub fn cask_store_version_path(&self, cask_token: &str, version_str: &str) -> PathBuf {
        self.cask_store_token_path(cask_token).join(version_str)
    }

    pub fn cask_store_app_path(
        &self,
        cask_token: &str,
        version_str: &str,
        app_name: &str,
    ) -> PathBuf {
        self.cask_store_version_path(cask_token, version_str)
            .join(app_name)
    }

    pub fn cask_room_version_path(&self, cask_token: &str, version_str: &str) -> PathBuf {
        self.cask_room_token_path(cask_token).join(version_str)
    }

    pub fn home_dir(&self) -> PathBuf {
        UserDirs::new().map_or_else(|| PathBuf::from("/"), |ud| ud.home_dir().to_path_buf())
    }

    pub fn get_tap_path(&self, name: &str) -> Option<PathBuf> {
        let parts: Vec<&str> = name.split('/').collect();
        if parts.len() == 2 {
            Some(
                self.taps_dir()
                    .join(parts[0]) // user, e.g., homebrew
                    .join(format!("homebrew-{}", parts[1])), // repo, e.g., homebrew-core
            )
        } else {
            None
        }
    }

    pub fn get_formula_path_from_tap(&self, tap_name: &str, formula_name: &str) -> Option<PathBuf> {
        self.get_tap_path(tap_name).and_then(|tap_path| {
            let json_path = tap_path
                .join("Formula") // Standard Homebrew tap structure
                .join(format!("{formula_name}.json"));
            if json_path.exists() {
                return Some(json_path);
            }
            // Fallback to .rb for completeness, though API primarily gives JSON
            let rb_path = tap_path.join("Formula").join(format!("{formula_name}.rb"));
            if rb_path.exists() {
                return Some(rb_path);
            }
            None
        })
    }
}

impl Default for Config {
    fn default() -> Self {
        Self::load().expect("Failed to load default configuration")
    }
}

pub fn load_config() -> Result<Config> {
    Config::load()
}

use std::path::Path;
use std::process::Command;
use std::thread;
use std::time::Duration;

use plist::Value as PlistValue;
use sps_common::error::Result;
use tracing::{debug, warn};

fn get_bundle_identifier_from_app_path(app_path: &Path) -> Option<String> {
    let info_plist_path = app_path.join("Contents/Info.plist");
    if !info_plist_path.is_file() {
        debug!("Info.plist not found at {}", info_plist_path.display());
        return None;
    }
    match PlistValue::from_file(&info_plist_path) {
        Ok(PlistValue::Dictionary(dict)) => dict
            .get("CFBundleIdentifier")
            .and_then(PlistValue::as_string)
            .map(String::from),
        Ok(val) => {
            warn!(
                "Info.plist at {} is not a dictionary. Value: {:?}",
                info_plist_path.display(),
                val
            );
            None
        }
        Err(e) => {
            warn!(
                "Failed to parse Info.plist at {}: {}",
                info_plist_path.display(),
                e
            );
            None
        }
    }
}

fn is_app_running_by_bundle_id(bundle_id: &str) -> Result<bool> {
    let script = format!(
        "tell application \"System Events\" to (exists (process 1 where bundle identifier is \"{bundle_id}\"))"
    );
    debug!(
        "Checking if app with bundle ID '{}' is running using script: {}",
        bundle_id, script
    );

    let output = Command::new("osascript").arg("-e").arg(&script).output()?;

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout)
            .trim()
            .to_lowercase();
        debug!(
            "is_app_running_by_bundle_id ('{}') stdout: '{}'",
            bundle_id, stdout
        );
        Ok(stdout == "true")
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        warn!(
            "osascript check running status for bundle ID '{}' failed. Status: {}, Stderr: {}",
            bundle_id,
            output.status,
            stderr.trim()
        );
        Ok(false)
    }
}

/// Attempts to gracefully quit an application using its bundle identifier (preferred) or name via
/// AppleScript. Retries several times, checking if the app is still running between attempts.
/// Returns Ok even if the app could not be quit, as uninstall should proceed.
pub fn quit_app_gracefully(app_path: &Path) -> Result<()> {
    if !cfg!(target_os = "macos") {
        debug!("Not on macOS, skipping app quit for {}", app_path.display());
        return Ok(());
    }
    if !app_path.exists() {
        debug!(
            "App path {} does not exist, skipping quit attempt.",
            app_path.display()
        );
        return Ok(());
    }

    let app_name_for_log = app_path
        .file_name()
        .map_or_else(|| app_path.to_string_lossy(), |name| name.to_string_lossy())
        .trim_end_matches(".app")
        .to_string();

    let bundle_identifier = get_bundle_identifier_from_app_path(app_path);

    let (script_target, using_bundle_id) = match &bundle_identifier {
        Some(id) => (id.clone(), true),
        None => {
            warn!(
                "Could not get bundle identifier for {}. Will attempt to quit by name '{}'. This is less reliable.",
                app_path.display(),
                app_name_for_log
            );
            (app_name_for_log.clone(), false)
        }
    };

    debug!(
        "Attempting to quit app '{}' (script target: '{}', using bundle_id: {})",
        app_name_for_log, script_target, using_bundle_id
    );

    // Initial check if app is running (only reliable if we have bundle ID)
    if using_bundle_id {
        match is_app_running_by_bundle_id(&script_target) {
            Ok(true) => debug!(
                "App '{}' is running. Proceeding with quit attempts.",
                script_target
            ),
            Ok(false) => {
                debug!("App '{}' is not running. Quit unnecessary.", script_target);
                return Ok(());
            }
            Err(e) => {
                warn!(
                    "Could not determine if app '{}' is running (check failed: {}). Proceeding with quit attempt.",
                    script_target, e
                );
            }
        }
    }

    let quit_command = if using_bundle_id {
        format!("tell application id \"{script_target}\" to quit")
    } else {
        format!("tell application \"{script_target}\" to quit")
    };

    const MAX_QUIT_ATTEMPTS: usize = 4;
    const QUIT_DELAYS_SECS: [u64; MAX_QUIT_ATTEMPTS - 1] = [2, 3, 5];

    // Use enumerate over QUIT_DELAYS_SECS for Clippy compliance
    for (attempt, delay) in QUIT_DELAYS_SECS.iter().enumerate() {
        debug!(
            "Quit attempt #{} for '{}' using command: {}",
            attempt + 1,
            script_target,
            quit_command
        );

        let output = Command::new("osascript")
            .arg("-e")
            .arg(&quit_command)
            .output()?; // Propagate IO errors from osascript execution

        if output.status.success() {
            debug!(
                "osascript quit command sent successfully for '{}' (attempt #{}).",
                script_target,
                attempt + 1
            );
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr);
            if stderr.contains("-1712")
                || stderr.contains("-1728")
                || stderr.contains("-600")
                || stderr.to_lowercase().contains("application isn’t running")
                || stderr.to_lowercase().contains("application not running")
            {
                debug!(
                    "osascript: App '{}' reported as not running or not found (stderr: {}). Quit successful or unnecessary.",
                    script_target,
                    stderr.trim()
                );
                return Ok(());
            } else {
                warn!(
                    "osascript quit command for '{}' failed (attempt #{}). Status: {}. Stderr: {}",
                    script_target,
                    attempt + 1,
                    output.status,
                    stderr.trim()
                );
            }
        }

        // Wait briefly to allow the app to process the quit command
        thread::sleep(Duration::from_secs(*delay));

        // Check if the app is still running (if using bundle ID)
        if using_bundle_id {
            match is_app_running_by_bundle_id(&script_target) {
                Ok(true) => {
                    debug!(
                        "App '{}' still running after attempt #{}. Retrying.",
                        script_target,
                        attempt + 1
                    );
                }
                Ok(false) => {
                    debug!(
                        "App '{}' successfully quit after attempt #{}.",
                        script_target,
                        attempt + 1
                    );
                    return Ok(());
                }
                Err(e) => {
                    warn!(
                        "Could not verify if app '{}' quit after attempt #{}: {}. Assuming it might still be running.",
                        script_target,
                        attempt + 1,
                        e
                    );
                }
            }
        }
    }

    // Final attempt (the fourth, not covered by QUIT_DELAYS_SECS)
    let attempt = QUIT_DELAYS_SECS.len();
    debug!(
        "Quit attempt #{} for '{}' using command: {}",
        attempt + 1,
        script_target,
        quit_command
    );

    let output = Command::new("osascript")
        .arg("-e")
        .arg(&quit_command)
        .output()?; // Propagate IO errors from osascript execution

    if output.status.success() {
        debug!(
            "osascript quit command sent successfully for '{}' (attempt #{}).",
            script_target,
            attempt + 1
        );
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        if stderr.contains("-1712")
            || stderr.contains("-1728")
            || stderr.contains("-600")
            || stderr.to_lowercase().contains("application isn’t running")
            || stderr.to_lowercase().contains("application not running")
        {
            debug!(
                "osascript: App '{}' reported as not running or not found (stderr: {}). Quit successful or unnecessary.",
                script_target,
                stderr.trim()
            );
            return Ok(());
        } else {
            warn!(
                "osascript quit command for '{}' failed (attempt #{}). Status: {}. Stderr: {}",
                script_target,
                attempt + 1,
                output.status,
                stderr.trim()
            );
        }
    }

    // Final check if the app is still running (if using bundle ID)
    if using_bundle_id {
        match is_app_running_by_bundle_id(&script_target) {
            Ok(true) => {
                warn!(
                    "App '{}' still running after {} quit attempts.",
                    script_target, MAX_QUIT_ATTEMPTS
                );
            }
            Ok(false) => {
                debug!(
                    "App '{}' successfully quit after attempt #{}.",
                    script_target,
                    attempt + 1
                );
                return Ok(());
            }
            Err(e) => {
                warn!(
                    "Could not verify if app '{}' quit after attempt #{}: {}. Assuming it might still be running.",
                    script_target,
                    attempt + 1,
                    e
                );
            }
        }
    } else {
        warn!(
            "App '{}' (targeted by name) might still be running after {} quit attempts. Manual check may be needed.",
            script_target,
            MAX_QUIT_ATTEMPTS
        );
    }
    Ok(())
}

use std::sync::Arc;

use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT};
use reqwest::Client;
use serde_json::Value;
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use sps_common::model::cask::{Cask, CaskList};
use sps_common::model::formula::Formula;
use tracing::{debug, error};

const FORMULAE_API_BASE_URL: &str = "https://formulae.brew.sh/api";
const GITHUB_API_BASE_URL: &str = "https://api.github.com";
const USER_AGENT_STRING: &str = "sps Package Manager (Rust; +https://github.com/your/sp)";

fn build_api_client(config: &Config) -> Result<Client> {
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(USER_AGENT, USER_AGENT_STRING.parse().unwrap());
    headers.insert(ACCEPT, "application/vnd.github+json".parse().unwrap());
    if let Some(token) = &config.github_api_token {
        debug!("Adding GitHub API token to request headers.");
        match format!("Bearer {token}").parse() {
            Ok(val) => {
                headers.insert(AUTHORIZATION, val);
            }
            Err(e) => {
                error!("Failed to parse GitHub API token into header value: {}", e);
            }
        }
    } else {
        debug!("No GitHub API token found in config.");
    }
    Ok(Client::builder().default_headers(headers).build()?)
}

pub async fn fetch_raw_formulae_json(endpoint: &str) -> Result<String> {
    let url = format!("{FORMULAE_API_BASE_URL}/{endpoint}");
    debug!("Fetching data from Homebrew Formulae API: {}", url);
    let client = reqwest::Client::builder()
        .user_agent(USER_AGENT_STRING)
        .build()?;
    let response = client.get(&url).send().await.map_err(|e| {
        debug!("HTTP request failed for {}: {}", url, e);
        SpsError::Http(Arc::new(e))
    })?;
    if !response.status().is_success() {
        let status = response.status();
        let body = response
            .text()
            .await
            .unwrap_or_else(|e| format!("(Failed to read response body: {e})"));
        debug!(
            "HTTP request to {} returned non-success status: {}",
            url, status
        );
        debug!("Response body for failed request to {}: {}", url, body);
        return Err(SpsError::Api(format!("HTTP status {status} from {url}")));
    }
    let body = response.text().await?;
    if body.trim().is_empty() {
        error!("Response body for {} was empty.", url);
        return Err(SpsError::Api(format!(
            "Empty response body received from {url}"
        )));
    }
    Ok(body)
}

pub async fn fetch_all_formulas() -> Result<String> {
    fetch_raw_formulae_json("formula.json").await
}

pub async fn fetch_all_casks() -> Result<String> {
    fetch_raw_formulae_json("cask.json").await
}

pub async fn fetch_formula(name: &str) -> Result<serde_json::Value> {
    let direct_fetch_result = fetch_raw_formulae_json(&format!("formula/{name}.json")).await;
    if let Ok(body) = direct_fetch_result {
        let formula: serde_json::Value = serde_json::from_str(&body)?;
        Ok(formula)
    } else {
        debug!(
            "Direct fetch for formula '{}' failed ({:?}). Fetching full list as fallback.",
            name,
            direct_fetch_result.err()
        );
        let all_formulas_body = fetch_all_formulas().await?;
        let formulas: Vec<serde_json::Value> = serde_json::from_str(&all_formulas_body)?;
        for formula in formulas {
            if formula.get("name").and_then(Value::as_str) == Some(name) {
                return Ok(formula);
            }
            if formula.get("full_name").and_then(Value::as_str) == Some(name) {
                return Ok(formula);
            }
        }
        Err(SpsError::NotFound(format!(
            "Formula '{name}' not found in API list"
        )))
    }
}

pub async fn fetch_cask(token: &str) -> Result<serde_json::Value> {
    let direct_fetch_result = fetch_raw_formulae_json(&format!("cask/{token}.json")).await;
    if let Ok(body) = direct_fetch_result {
        let cask: serde_json::Value = serde_json::from_str(&body)?;
        Ok(cask)
    } else {
        debug!(
            "Direct fetch for cask '{}' failed ({:?}). Fetching full list as fallback.",
            token,
            direct_fetch_result.err()
        );
        let all_casks_body = fetch_all_casks().await?;
        let casks: Vec<serde_json::Value> = serde_json::from_str(&all_casks_body)?;
        for cask in casks {
            if cask.get("token").and_then(Value::as_str) == Some(token) {
                return Ok(cask);
            }
        }
        Err(SpsError::NotFound(format!(
            "Cask '{token}' not found in API list"
        )))
    }
}

async fn fetch_github_api_json(endpoint: &str, config: &Config) -> Result<Value> {
    let url = format!("{GITHUB_API_BASE_URL}{endpoint}");
    debug!("Fetching data from GitHub API: {}", url);
    let client = build_api_client(config)?;
    let response = client.get(&url).send().await.map_err(|e| {
        error!("GitHub API request failed for {}: {}", url, e);
        SpsError::Http(Arc::new(e))
    })?;
    if !response.status().is_success() {
        let status = response.status();
        let body = response
            .text()
            .await
            .unwrap_or_else(|e| format!("(Failed to read response body: {e})"));
        error!(
            "GitHub API request to {} returned non-success status: {}",
            url, status
        );
        debug!(
            "Response body for failed GitHub API request to {}: {}",
            url, body
        );
        return Err(SpsError::Api(format!("HTTP status {status} from {url}")));
    }
    let value: Value = response.json::<Value>().await.map_err(|e| {
        error!("Failed to parse JSON response from {}: {}", url, e);
        SpsError::ApiRequestError(e.to_string())
    })?;
    Ok(value)
}

#[allow(dead_code)]
async fn fetch_github_repo_info(owner: &str, repo: &str, config: &Config) -> Result<Value> {
    let endpoint = format!("/repos/{owner}/{repo}");
    fetch_github_api_json(&endpoint, config).await
}

pub async fn get_formula(name: &str) -> Result<Formula> {
    let url = format!("{FORMULAE_API_BASE_URL}/formula/{name}.json");
    debug!(
        "Fetching and parsing formula data for '{}' from {}",
        name, url
    );
    let client = reqwest::Client::new();
    let response = client.get(&url).send().await.map_err(|e| {
        debug!("HTTP request failed when fetching formula {}: {}", name, e);
        SpsError::Http(Arc::new(e))
    })?;
    let status = response.status();
    let text = response.text().await?;
    if !status.is_success() {
        debug!("Failed to fetch formula {} (Status {})", name, status);
        debug!("Response body for failed formula fetch {}: {}", name, text);
        return Err(SpsError::Api(format!(
            "Failed to fetch formula {name}: Status {status}"
        )));
    }
    if text.trim().is_empty() {
        error!("Received empty body when fetching formula {}", name);
        return Err(SpsError::Api(format!(
            "Empty response body for formula {name}"
        )));
    }
    match serde_json::from_str::<Formula>(&text) {
        Ok(formula) => Ok(formula),
        Err(_) => match serde_json::from_str::<Vec<Formula>>(&text) {
            Ok(mut formulas) if !formulas.is_empty() => {
                debug!(
                    "Parsed formula {} from a single-element array response.",
                    name
                );
                Ok(formulas.remove(0))
            }
            Ok(_) => {
                error!("Received empty array when fetching formula {}", name);
                Err(SpsError::NotFound(format!(
                    "Formula '{name}' not found (empty array returned)"
                )))
            }
            Err(e_vec) => {
                error!(
                    "Failed to parse formula {} as object or array. Error: {}. Body (sample): {}",
                    name,
                    e_vec,
                    text.chars().take(500).collect::<String>()
                );
                Err(SpsError::Json(Arc::new(e_vec)))
            }
        },
    }
}

pub async fn get_all_formulas() -> Result<Vec<Formula>> {
    let raw_data = fetch_all_formulas().await?;
    serde_json::from_str(&raw_data).map_err(|e| {
        error!("Failed to parse all_formulas response: {}", e);
        SpsError::Json(Arc::new(e))
    })
}

pub async fn get_cask(name: &str) -> Result<Cask> {
    let raw_json_result = fetch_cask(name).await;
    let raw_json = match raw_json_result {
        Ok(json_val) => json_val,
        Err(e) => {
            error!("Failed to fetch raw JSON for cask {}: {}", name, e);
            return Err(e);
        }
    };
    match serde_json::from_value::<Cask>(raw_json.clone()) {
        Ok(cask) => Ok(cask),
        Err(e) => {
            error!("Failed to parse cask {} JSON: {}", name, e);
            match serde_json::to_string_pretty(&raw_json) {
                Ok(json_str) => {
                    tracing::debug!("Problematic JSON for cask '{}':\n{}", name, json_str);
                }
                Err(fmt_err) => {
                    tracing::debug!(
                        "Could not pretty-print problematic JSON for cask {}: {}",
                        name,
                        fmt_err
                    );
                    tracing::debug!("Raw problematic value: {:?}", raw_json);
                }
            }
            Err(SpsError::Json(Arc::new(e)))
        }
    }
}

pub async fn get_all_casks() -> Result<CaskList> {
    let raw_data = fetch_all_casks().await?;
    let casks: Vec<Cask> = serde_json::from_str(&raw_data).map_err(|e| {
        error!("Failed to parse all_casks response: {}", e);
        SpsError::Json(Arc::new(e))
    })?;
    Ok(CaskList { casks })
}

use std::collections::HashMap;
use std::fs::{remove_file, File};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;

use futures::StreamExt;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use reqwest::header::{ACCEPT, AUTHORIZATION};
use reqwest::{Client, Response, StatusCode};
use serde::{Deserialize, Serialize};
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use tracing::{debug, error};
use url::Url;

use crate::http::ProgressCallback;
use crate::validation::{validate_url, verify_checksum};

const OCI_MANIFEST_V1_TYPE: &str = "application/vnd.oci.image.index.v1+json";
const OCI_LAYER_V1_TYPE: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
const DEFAULT_GHCR_TOKEN_ENDPOINT: &str = "https://ghcr.io/token";
pub const DEFAULT_GHCR_DOMAIN: &str = "ghcr.io";

const CONNECT_TIMEOUT_SECS: u64 = 30;
const REQUEST_TIMEOUT_SECS: u64 = 300;
const USER_AGENT_STRING: &str = "sps package manager (Rust; +https://github.com/alexykn/sps)";

#[derive(Deserialize, Debug)]
struct OciTokenResponse {
    token: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OciManifestIndex {
    pub schema_version: u32,
    pub media_type: Option<String>,
    pub manifests: Vec<OciManifestDescriptor>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OciManifestDescriptor {
    pub media_type: String,
    pub digest: String,
    pub size: u64,
    pub platform: Option<OciPlatform>,
    pub annotations: Option<HashMap<String, String>>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OciPlatform {
    pub architecture: String,
    pub os: String,
    #[serde(rename = "os.version")]
    pub os_version: Option<String>,
    #[serde(default)]
    pub features: Vec<String>,
    pub variant: Option<String>,
}

#[derive(Debug, Clone)]
enum OciAuth {
    None,
    AnonymousBearer { token: String },
    ExplicitBearer { token: String },
    Basic { encoded: String },
}

async fn fetch_oci_resource<T: serde::de::DeserializeOwned>(
    resource_url: &str,
    accept_header: &str,
    config: &Config,
    client: &Client,
) -> Result<T> {
    let url = Url::parse(resource_url)
        .map_err(|e| SpsError::Generic(format!("Invalid URL '{resource_url}': {e}")))?;
    validate_url(url.as_str())?;
    let registry_domain = url.host_str().unwrap_or(DEFAULT_GHCR_DOMAIN);
    let repo_path = extract_repo_path_from_url(&url).unwrap_or("");

    let auth = determine_auth(config, client, registry_domain, repo_path).await?;
    let resp = execute_oci_request(client, resource_url, accept_header, &auth).await?;
    let txt = resp.text().await.map_err(|e| SpsError::Http(Arc::new(e)))?;

    debug!("OCI response ({} bytes) from {}", txt.len(), resource_url);
    serde_json::from_str(&txt).map_err(|e| {
        error!("JSON parse error from {}: {}", resource_url, e);
        SpsError::Json(Arc::new(e))
    })
}

pub async fn download_oci_blob(
    blob_url: &str,
    destination_path: &Path,
    config: &Config,
    client: &Client,
    expected_digest: &str,
) -> Result<()> {
    download_oci_blob_with_progress(
        blob_url,
        destination_path,
        config,
        client,
        expected_digest,
        None,
    )
    .await
}

pub async fn download_oci_blob_with_progress(
    blob_url: &str,
    destination_path: &Path,
    config: &Config,
    client: &Client,
    expected_digest: &str,
    progress_callback: Option<ProgressCallback>,
) -> Result<()> {
    debug!("Downloading OCI blob: {}", blob_url);
    let url = Url::parse(blob_url)
        .map_err(|e| SpsError::Generic(format!("Invalid URL '{blob_url}': {e}")))?;
    validate_url(url.as_str())?;
    let registry_domain = url.host_str().unwrap_or(DEFAULT_GHCR_DOMAIN);
    let repo_path = extract_repo_path_from_url(&url).unwrap_or("");

    let auth = determine_auth(config, client, registry_domain, repo_path).await?;
    let resp = execute_oci_request(client, blob_url, OCI_LAYER_V1_TYPE, &auth).await?;

    // Get total size from Content-Length header if available
    let total_size = resp.content_length();

    let tmp = destination_path.with_file_name(format!(
        ".{}.download",
        destination_path.file_name().unwrap().to_string_lossy()
    ));
    let mut out = File::create(&tmp).map_err(|e| SpsError::Io(Arc::new(e)))?;

    let mut stream = resp.bytes_stream();
    let mut bytes_downloaded = 0u64;

    while let Some(chunk) = stream.next().await {
        let b = chunk.map_err(|e| SpsError::Http(Arc::new(e)))?;
        std::io::Write::write_all(&mut out, &b).map_err(|e| SpsError::Io(Arc::new(e)))?;

        bytes_downloaded += b.len() as u64;

        // Call progress callback if provided
        if let Some(ref callback) = progress_callback {
            callback(bytes_downloaded, total_size);
        }
    }
    std::fs::rename(&tmp, destination_path).map_err(|e| SpsError::Io(Arc::new(e)))?;

    if !expected_digest.is_empty() {
        match verify_checksum(destination_path, expected_digest) {
            Ok(_) => {
                tracing::debug!("OCI Blob checksum verified: {}", destination_path.display());
            }
            Err(e) => {
                tracing::error!(
                    "OCI Blob checksum mismatch ({}). Deleting downloaded file.",
                    e
                );
                let _ = remove_file(destination_path);
                return Err(e);
            }
        }
    } else {
        tracing::warn!(
            "Skipping checksum verification for OCI blob {} - no checksum provided.",
            destination_path.display()
        );
    }

    debug!("Blob saved to {}", destination_path.display());
    Ok(())
}

pub async fn fetch_oci_manifest_index(
    manifest_url: &str,
    config: &Config,
    client: &Client,
) -> Result<OciManifestIndex> {
    fetch_oci_resource(manifest_url, OCI_MANIFEST_V1_TYPE, config, client).await
}

pub fn build_oci_client() -> Result<Client> {
    Client::builder()
        .user_agent(USER_AGENT_STRING)
        .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
        .timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
        .redirect(reqwest::redirect::Policy::default())
        .build()
        .map_err(|e| SpsError::Http(Arc::new(e)))
}

fn extract_repo_path_from_url(url: &Url) -> Option<&str> {
    url.path()
        .trim_start_matches('/')
        .trim_start_matches("v2/")
        .split("/manifests/")
        .next()
        .and_then(|s| s.split("/blobs/").next())
        .filter(|s| !s.is_empty())
}

async fn determine_auth(
    config: &Config,
    client: &Client,
    registry_domain: &str,
    repo_path: &str,
) -> Result<OciAuth> {
    if let Some(token) = &config.docker_registry_token {
        debug!("Using explicit bearer for {}", registry_domain);
        return Ok(OciAuth::ExplicitBearer {
            token: token.clone(),
        });
    }
    if let Some(basic) = &config.docker_registry_basic_auth {
        debug!("Using explicit basic auth for {}", registry_domain);
        return Ok(OciAuth::Basic {
            encoded: basic.clone(),
        });
    }

    if registry_domain.eq_ignore_ascii_case(DEFAULT_GHCR_DOMAIN) && !repo_path.is_empty() {
        debug!(
            "Anonymous token fetch for {} scope={}",
            registry_domain, repo_path
        );
        match fetch_anonymous_token(client, registry_domain, repo_path).await {
            Ok(t) => return Ok(OciAuth::AnonymousBearer { token: t }),
            Err(e) => debug!("Anon token failed, proceeding unauthenticated: {}", e),
        }
    }
    Ok(OciAuth::None)
}

async fn fetch_anonymous_token(
    client: &Client,
    registry_domain: &str,
    repo_path: &str,
) -> Result<String> {
    let endpoint = if registry_domain.eq_ignore_ascii_case(DEFAULT_GHCR_DOMAIN) {
        DEFAULT_GHCR_TOKEN_ENDPOINT.to_string()
    } else {
        format!("https://{registry_domain}/token")
    };
    let scope = format!("repository:{repo_path}:pull");
    let token_url = format!("{endpoint}?service={registry_domain}&scope={scope}");

    const MAX_RETRIES: u8 = 3;
    let base_delay = Duration::from_millis(200);
    let mut delay = base_delay;
    // Use a Sendable RNG
    let mut rng = SmallRng::from_os_rng();

    for attempt in 0..=MAX_RETRIES {
        debug!(
            "Token attempt {}/{} from {}",
            attempt + 1,
            MAX_RETRIES + 1,
            token_url
        );

        match client.get(&token_url).send().await {
            Ok(resp) if resp.status().is_success() => {
                let tok: OciTokenResponse = resp
                    .json()
                    .await
                    .map_err(|e| SpsError::ApiRequestError(format!("Parse token response: {e}")))?;
                return Ok(tok.token);
            }
            Ok(resp) => {
                let code = resp.status();
                let body = resp.text().await.unwrap_or_default();
                error!("Token fetch {}: {} – {}", attempt + 1, code, body);
                if !code.is_server_error() || attempt == MAX_RETRIES {
                    return Err(SpsError::Api(format!("Token endpoint {code}: {body}")));
                }
            }
            Err(e) => {
                error!("Network error on token fetch {}: {}", attempt + 1, e);
                if attempt == MAX_RETRIES {
                    return Err(SpsError::Http(Arc::new(e)));
                }
            }
        }

        let jitter = rng.random_range(0..(base_delay.as_millis() as u64 / 2));
        tokio::time::sleep(delay + Duration::from_millis(jitter)).await;
        delay *= 2;
    }

    Err(SpsError::Api(format!(
        "Failed to fetch OCI token after {} attempts",
        MAX_RETRIES + 1
    )))
}

async fn execute_oci_request(
    client: &Client,
    url: &str,
    accept: &str,
    auth: &OciAuth,
) -> Result<Response> {
    debug!("OCI request → {} (Accept: {})", url, accept);
    let mut req = client.get(url).header(ACCEPT, accept);
    match auth {
        OciAuth::AnonymousBearer { token } | OciAuth::ExplicitBearer { token }
            if !token.is_empty() =>
        {
            req = req.header(AUTHORIZATION, format!("Bearer {token}"))
        }
        OciAuth::Basic { encoded } if !encoded.is_empty() => {
            req = req.header(AUTHORIZATION, format!("Basic {encoded}"))
        }
        _ => {}
    }

    let resp = req.send().await.map_err(|e| SpsError::Http(Arc::new(e)))?;
    let status = resp.status();
    if status.is_success() {
        Ok(resp)
    } else {
        let body = resp.text().await.unwrap_or_default();
        error!("OCI {} ⇒ {} – {}", url, status, body);
        let err = match status {
            StatusCode::UNAUTHORIZED => SpsError::Api(format!("Auth required: {status}")),
            StatusCode::FORBIDDEN => SpsError::Api(format!("Permission denied: {status}")),
            StatusCode::NOT_FOUND => SpsError::NotFound(format!("Not found: {status}")),
            _ => SpsError::Api(format!("HTTP {status} – {body}")),
        };
        Err(err)
    }
}

use std::path::Path;
use std::process::Command;

use anyhow::Context;
use sps_common::error::{Result, SpsError};
use tracing::{debug, error};
use uuid::Uuid;
use xattr;

// Helper to get current timestamp as hex
fn get_timestamp_hex() -> String {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default() // Defaults to 0 if time is before UNIX_EPOCH
        .as_secs();
    format!("{secs:x}")
}

// Helper to generate a UUID as hex string
fn get_uuid_hex() -> String {
    Uuid::new_v4().as_hyphenated().to_string().to_uppercase()
}

/// true  → file **has** a com.apple.quarantine attribute
/// false → attribute missing
pub fn has_quarantine_attribute(path: &Path) -> anyhow::Result<bool> {
    // The `xattr` crate has both path-level and FileExt APIs.
    // Path-level is simpler here.
    match xattr::get(path, "com.apple.quarantine") {
        Ok(Some(_)) => Ok(true),
        Ok(None) => Ok(false),
        Err(e) => Err(anyhow::Error::new(e))
            .with_context(|| format!("checking xattr on {}", path.display())),
    }
}

/// Apply our standard quarantine only *if* none exists already.
///
/// `agent` should be the same string you currently pass to
/// `set_quarantine_attribute()` – usually the cask token.
pub fn ensure_quarantine_attribute(path: &Path, agent: &str) -> anyhow::Result<()> {
    if has_quarantine_attribute(path)? {
        // Already quarantined (or the user cleared it and we respect that) → done
        return Ok(());
    }
    set_quarantine_attribute(path, agent)
        .with_context(|| format!("adding quarantine to {}", path.display()))
}

/// Sets the 'com.apple.quarantine' extended attribute on a file or directory.
/// Uses flags commonly seen for user-initiated downloads (0081).
/// Logs errors assertively, as failure is critical for correct behavior.
pub fn set_quarantine_attribute(path: &Path, agent_name: &str) -> Result<()> {
    if !cfg!(target_os = "macos") {
        debug!(
            "Not on macOS, skipping quarantine attribute for {}",
            path.display()
        );
        return Ok(());
    }

    if !path.exists() {
        error!(
            "Cannot set quarantine attribute, path does not exist: {}",
            path.display()
        );
        return Err(SpsError::NotFound(format!(
            "Path not found for setting quarantine attribute: {}",
            path.display()
        )));
    }

    let timestamp_hex = get_timestamp_hex();
    let uuid_hex = get_uuid_hex();
    // Use "0181" to disable translocation and quarantine mirroring (Homebrew-style).
    // Format: "flags;timestamp_hex;agent_name;uuid_hex"
    let quarantine_value = format!("0181;{timestamp_hex};{agent_name};{uuid_hex}");

    debug!(
        "Setting quarantine attribute on {}: value='{}'",
        path.display(),
        quarantine_value
    );

    let output = Command::new("xattr")
        .arg("-w")
        .arg("com.apple.quarantine")
        .arg(&quarantine_value)
        .arg(path.as_os_str())
        .output();

    match output {
        Ok(out) => {
            if out.status.success() {
                debug!(
                    "Successfully set quarantine attribute for {}",
                    path.display()
                );
                Ok(())
            } else {
                let stderr = String::from_utf8_lossy(&out.stderr);
                error!( // Changed from warn to error as this is critical for the bug
                    "Failed to set quarantine attribute for {} (status: {}): {}. This may lead to data loss on reinstall or Gatekeeper issues.",
                    path.display(),
                    out.status,
                    stderr.trim()
                );
                // Return an error because failure to set this is likely to cause the reported bug
                Err(SpsError::Generic(format!(
                    "Failed to set com.apple.quarantine on {}: {}",
                    path.display(),
                    stderr.trim()
                )))
            }
        }
        Err(e) => {
            error!(
                "Failed to execute xattr command for {}: {}. Quarantine attribute not set.",
                path.display(),
                e
            );
            Err(SpsError::Io(std::sync::Arc::new(e)))
        }
    }
}

// sps-core/src/upgrade/cask.rs

use std::path::Path;

use sps_common::config::Config;
use sps_common::error::{Result as SpsResult, SpsError};
use sps_common::model::cask::Cask;
use sps_common::pipeline::JobAction; // Required for install_cask
use tracing::{debug, error};

use crate::check::installed::InstalledPackageInfo;
use crate::{install, uninstall};

/// Upgrades a cask package using Homebrew's proven strategy.
pub async fn upgrade_cask_package(
    cask: &Cask,
    new_cask_download_path: &Path,
    old_install_info: &InstalledPackageInfo,
    config: &Config,
) -> SpsResult<()> {
    debug!(
        "Upgrading cask {} from {} to {}",
        cask.token,
        old_install_info.version,
        cask.version.as_deref().unwrap_or("latest")
    );

    // 1. Soft-uninstall the old version
    // This removes linked artifacts and updates the old manifest's is_installed flag.
    // It does not remove the old Caskroom version directory itself yet.
    debug!(
        "Soft-uninstalling old cask version: {} at {}",
        old_install_info.version,
        old_install_info.path.display()
    );
    uninstall::cask::uninstall_cask_artifacts(old_install_info, config).map_err(|e| {
        error!(
            "Failed to soft-uninstall old version {} of cask {}: {}",
            old_install_info.version, cask.token, e
        );
        SpsError::InstallError(format!(
            "Failed to soft-uninstall old version during upgrade of {}: {e}",
            cask.token
        ))
    })?;
    debug!(
        "Successfully soft-uninstalled old version of {}",
        cask.token
    );

    // 2. Install the new version
    // The install_cask function, particularly install_app_from_staged,
    // should handle the upgrade logic (like syncing app data) when
    // passed the JobAction::Upgrade.
    debug!(
        "Installing new version for cask {} from {}",
        cask.token,
        new_cask_download_path.display()
    );

    let job_action_for_install = JobAction::Upgrade {
        from_version: old_install_info.version.clone(),
        old_install_path: old_install_info.path.clone(),
    };

    install::cask::install_cask(
        cask,
        new_cask_download_path,
        config,
        &job_action_for_install,
    )
    .map_err(|e| {
        error!(
            "Failed to install new version of cask {}: {}",
            cask.token, e
        );
        SpsError::InstallError(format!(
            "Failed to install new version during upgrade of {}: {e}",
            cask.token
        ))
    })?;
    debug!("Successfully installed new version of cask {}", cask.token);

    Ok(())
}

// sps/src/cli/status.rs
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
use std::time::Instant;

use colored::*;
use sps_common::config::Config;
use sps_common::pipeline::{PipelineEvent, PipelinePackageType};
use tokio::sync::broadcast;
use tracing::debug;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum JobStatus {
    Waiting,
    Downloading,
    Downloaded,
    Cached,
    Processing,
    Installing,
    Linking,
    Success,
    Failed,
}

impl JobStatus {
    fn display_state(&self) -> &'static str {
        match self {
            JobStatus::Waiting => "waiting",
            JobStatus::Downloading => "downloading",
            JobStatus::Downloaded => "downloaded",
            JobStatus::Cached => "cached",
            JobStatus::Processing => "processing",
            JobStatus::Installing => "installing",
            JobStatus::Linking => "linking",
            JobStatus::Success => "success",
            JobStatus::Failed => "failed",
        }
    }

    fn slot_indicator(&self) -> String {
        match self {
            JobStatus::Waiting => " ⧗".yellow().to_string(),
            JobStatus::Downloading => " ⬇".blue().to_string(),
            JobStatus::Downloaded => " ✓".green().to_string(),
            JobStatus::Cached => " ⌂".cyan().to_string(),
            JobStatus::Processing => " ⚙".yellow().to_string(),
            JobStatus::Installing => " ⚙".cyan().to_string(),
            JobStatus::Linking => " →".magenta().to_string(),
            JobStatus::Success => " ✓".green().bold().to_string(),
            JobStatus::Failed => " ✗".red().bold().to_string(),
        }
    }

    fn colored_state(&self) -> ColoredString {
        match self {
            JobStatus::Waiting => self.display_state().dimmed(),
            JobStatus::Downloading => self.display_state().blue(),
            JobStatus::Downloaded => self.display_state().green(),
            JobStatus::Cached => self.display_state().cyan(),
            JobStatus::Processing => self.display_state().yellow(),
            JobStatus::Installing => self.display_state().yellow(),
            JobStatus::Linking => self.display_state().yellow(),
            JobStatus::Success => self.display_state().green(),
            JobStatus::Failed => self.display_state().red(),
        }
    }
}

struct JobInfo {
    name: String,
    status: JobStatus,
    size_bytes: Option<u64>,
    current_bytes_downloaded: Option<u64>,
    start_time: Option<Instant>,
    pool_id: usize,
}

impl JobInfo {
    fn _elapsed_str(&self) -> String {
        match self.start_time {
            Some(start) => format!("{:.1}s", start.elapsed().as_secs_f64()),
            None => "–".to_string(),
        }
    }

    fn size_str(&self) -> String {
        match self.size_bytes {
            Some(bytes) => format_bytes(bytes),
            None => "–".to_string(),
        }
    }
}

struct StatusDisplay {
    jobs: HashMap<String, JobInfo>,
    job_order: Vec<String>,
    total_jobs: usize,
    next_pool_id: usize,
    _start_time: Instant,
    active_downloads: HashSet<String>,
    total_bytes: u64,
    downloaded_bytes: u64,
    last_speed_update: Instant,
    last_aggregate_bytes_snapshot: u64,
    current_speed_bps: f64,
    _speed_history: Vec<f64>,
    header_printed: bool,
    last_line_count: usize,
}

impl StatusDisplay {
    fn new() -> Self {
        Self {
            jobs: HashMap::new(),
            job_order: Vec::new(),
            total_jobs: 0,
            next_pool_id: 1,
            _start_time: Instant::now(),
            active_downloads: HashSet::new(),
            total_bytes: 0,
            downloaded_bytes: 0,
            last_speed_update: Instant::now(),
            last_aggregate_bytes_snapshot: 0,
            current_speed_bps: 0.0,
            _speed_history: Vec::new(),
            header_printed: false,
            last_line_count: 0,
        }
    }

    fn add_job(&mut self, target_id: String, status: JobStatus, size_bytes: Option<u64>) {
        if !self.jobs.contains_key(&target_id) {
            let job_info = JobInfo {
                name: target_id.clone(),
                status,
                size_bytes,
                current_bytes_downloaded: if status == JobStatus::Downloading {
                    Some(0)
                } else {
                    None
                },
                start_time: if status != JobStatus::Waiting {
                    Some(Instant::now())
                } else {
                    None
                },
                pool_id: self.next_pool_id,
            };

            if let Some(bytes) = size_bytes {
                self.total_bytes += bytes;
            }

            if status == JobStatus::Downloading {
                self.active_downloads.insert(target_id.to_string());
            }

            self.jobs.insert(target_id.clone(), job_info);
            self.job_order.push(target_id);
            self.next_pool_id += 1;
        }
    }

    fn update_job_status(&mut self, target_id: &str, status: JobStatus, size_bytes: Option<u64>) {
        if let Some(job) = self.jobs.get_mut(target_id) {
            let was_downloading = job.status == JobStatus::Downloading;
            let is_downloading = status == JobStatus::Downloading;

            job.status = status;

            if job.start_time.is_none() && status != JobStatus::Waiting {
                job.start_time = Some(Instant::now());
            }

            if let Some(bytes) = size_bytes {
                if job.size_bytes.is_none() {
                    self.total_bytes += bytes;
                }
                job.size_bytes = Some(bytes);
            }

            // Update download counts
            if was_downloading && !is_downloading {
                self.active_downloads.remove(target_id);
                if let Some(bytes) = job.size_bytes {
                    job.current_bytes_downloaded = Some(bytes);
                    self.downloaded_bytes += bytes;
                }
            } else if !was_downloading && is_downloading {
                self.active_downloads.insert(target_id.to_string());
                job.current_bytes_downloaded = Some(0);
            }
        }
    }

    fn update_download_progress(
        &mut self,
        target_id: &str,
        bytes_so_far: u64,
        total_size: Option<u64>,
    ) {
        if let Some(job) = self.jobs.get_mut(target_id) {
            job.current_bytes_downloaded = Some(bytes_so_far);

            if let Some(total) = total_size {
                if job.size_bytes.is_none() {
                    // Update total bytes estimate
                    self.total_bytes += total;
                    job.size_bytes = Some(total);
                } else if job.size_bytes != Some(total) {
                    // Adjust total bytes if estimate changed
                    if let Some(old_size) = job.size_bytes {
                        self.total_bytes = self.total_bytes.saturating_sub(old_size) + total;
                    }
                    job.size_bytes = Some(total);
                }
            }
        }
    }

    fn update_speed(&mut self) {
        let now = Instant::now();
        let time_diff = now.duration_since(self.last_speed_update).as_secs_f64();

        if time_diff >= 0.0625 {
            // Calculate current total bytes for all jobs with current download progress
            let current_active_bytes: u64 = self
                .jobs
                .values()
                .filter(|job| matches!(job.status, JobStatus::Downloading))
                .map(|job| job.current_bytes_downloaded.unwrap_or(0))
                .sum();

            // Calculate bytes difference since last update
            let bytes_diff =
                current_active_bytes.saturating_sub(self.last_aggregate_bytes_snapshot);

            // Calculate speed
            if time_diff > 0.0 && bytes_diff > 0 {
                self.current_speed_bps = bytes_diff as f64 / time_diff;
            } else if !self
                .jobs
                .values()
                .any(|job| job.status == JobStatus::Downloading)
            {
                // No active downloads, reset speed to 0
                self.current_speed_bps = 0.0;
            }
            // If no bytes diff but still have active downloads, keep previous speed

            self.last_speed_update = now;
            self.last_aggregate_bytes_snapshot = current_active_bytes;
        }
    }

    fn render(&mut self) {
        self.update_speed();

        if !self.header_printed {
            // First render - print header and jobs
            self.print_header();
            let job_output = self.build_job_rows();
            print!("{job_output}");
            self.header_printed = true;
            // Count lines: header + jobs + separator + summary
            let job_lines = job_output.lines().count();
            self.last_line_count = 1 + job_lines + 1 + 1;
        } else {
            // Subsequent renders - clear and reprint header, job rows and summary
            self.clear_previous_output();
            self.print_header();
            let job_output = self.build_job_rows();
            print!("{job_output}");
            // Update line count (header + jobs + separator)
            let job_lines = job_output.lines().count();
            self.last_line_count = 1 + job_lines + 1;
        }

        // Print separator
        println!("{}", "─".repeat(49).dimmed());

        // Print status summary
        let completed = self
            .jobs
            .values()
            .filter(|j| matches!(j.status, JobStatus::Success))
            .count();
        let failed = self
            .jobs
            .values()
            .filter(|j| matches!(j.status, JobStatus::Failed))
            .count();
        let _progress_chars = self.generate_progress_bar(completed, failed);
        let _speed_str = format_speed(self.current_speed_bps);

        io::stdout().flush().unwrap();
    }

    fn print_header(&self) {
        println!(
            "{:<6} {:<12} {:<15} {:>8} {}",
            "IID".bold().dimmed(),
            "STATE".bold().dimmed(),
            "PKG".bold().dimmed(),
            "SIZE".bold().dimmed(),
            "SLOT".bold().dimmed()
        );
    }

    fn build_job_rows(&self) -> String {
        let mut output = String::new();

        // Job rows
        for target_id in &self.job_order {
            if let Some(job) = self.jobs.get(target_id) {
                let progress_str = if job.status == JobStatus::Downloading {
                    match (job.current_bytes_downloaded, job.size_bytes) {
                        (Some(downloaded), Some(_total)) => format_bytes(downloaded).to_string(),
                        (Some(downloaded), None) => format_bytes(downloaded),
                        _ => job.size_str(),
                    }
                } else {
                    job.size_str()
                };

                output.push_str(&format!(
                    "{:<6} {:<12} {:<15} {:>8} {}\n",
                    format!("#{:02}", job.pool_id).cyan(),
                    job.status.colored_state(),
                    job.name.cyan(),
                    progress_str,
                    job.status.slot_indicator()
                ));
            }
        }

        output
    }

    fn clear_previous_output(&self) {
        // Move cursor up and clear lines
        for _ in 0..self.last_line_count {
            print!("\x1b[1A\x1b[2K"); // Move up one line and clear it
        }
        io::stdout().flush().unwrap();
    }

    fn generate_progress_bar(&self, completed: usize, failed: usize) -> String {
        if self.total_jobs == 0 {
            return "".to_string();
        }

        let total_done = completed + failed;
        let progress_width = 8;
        let filled = (total_done * progress_width) / self.total_jobs;
        let remaining = progress_width - filled;

        let filled_str = "▍".repeat(filled).green();
        let remaining_str = "·".repeat(remaining).dimmed();

        format!("{filled_str}{remaining_str}")
    }
}

fn format_bytes(bytes: u64) -> String {
    const UNITS: &[&str] = &["B", "kB", "MB", "GB"];
    let mut value = bytes as f64;
    let mut unit_idx = 0;

    while value >= 1000.0 && unit_idx < UNITS.len() - 1 {
        value /= 1000.0;
        unit_idx += 1;
    }

    if unit_idx == 0 {
        format!("{bytes}B")
    } else {
        format!("{:.1}{}", value, UNITS[unit_idx])
    }
}

fn format_speed(bytes_per_sec: f64) -> String {
    if bytes_per_sec < 1.0 {
        return "0 B/s".to_string();
    }

    const UNITS: &[&str] = &["B/s", "kB/s", "MB/s", "GB/s"];
    let mut value = bytes_per_sec;
    let mut unit_idx = 0;

    while value >= 1000.0 && unit_idx < UNITS.len() - 1 {
        value /= 1000.0;
        unit_idx += 1;
    }

    format!("{:.1} {}", value, UNITS[unit_idx])
}

pub async fn handle_events(_config: Config, mut event_rx: broadcast::Receiver<PipelineEvent>) {
    let mut display = StatusDisplay::new();
    let mut logs_buffer = Vec::new();
    let mut pipeline_active = false;
    let mut refresh_interval = tokio::time::interval(tokio::time::Duration::from_millis(62));
    refresh_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

    loop {
        tokio::select! {
            _ = refresh_interval.tick() => {
                if pipeline_active && display.header_printed {
                    display.render();
                }
            }
            event_result = event_rx.recv() => {
                match event_result {
            Ok(event) => match event {
                PipelineEvent::PipelineStarted { total_jobs } => {
                    pipeline_active = true;
                    display.total_jobs = total_jobs;
                    println!("{}", "Starting pipeline.".cyan().bold());
                }
                PipelineEvent::PlanningStarted => {
                    debug!("{}", "Planning operations.".cyan());
                }
                PipelineEvent::DependencyResolutionStarted => {
                    println!("{}", "Resolving dependencies".cyan());
                }
                PipelineEvent::DependencyResolutionFinished => {
                    debug!("{}", "Dependency resolution complete.".cyan());
                }
                PipelineEvent::PlanningFinished { job_count } => {
                    println!("{} {}", "Planning finished. Jobs:".bold(), job_count);
                    println!();
                }
                PipelineEvent::DownloadStarted { target_id, url: _ } => {
                    display.add_job(target_id.clone(), JobStatus::Downloading, None);
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::DownloadFinished {
                    target_id,
                    size_bytes,
                    ..
                } => {
                    display.update_job_status(&target_id, JobStatus::Downloaded, Some(size_bytes));
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::DownloadProgressUpdate {
                    target_id,
                    bytes_so_far,
                    total_size,
                } => {
                    display.update_download_progress(&target_id, bytes_so_far, total_size);
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::DownloadCached {
                    target_id,
                    size_bytes,
                } => {
                    display.update_job_status(&target_id, JobStatus::Cached, Some(size_bytes));
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::DownloadFailed {
                    target_id, error, ..
                } => {
                    display.update_job_status(&target_id, JobStatus::Failed, None);
                    logs_buffer.push(format!(
                        "{} {}: {}",
                        "Download failed:".red(),
                        target_id.cyan(),
                        error.red()
                    ));
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::JobProcessingStarted { target_id } => {
                    display.update_job_status(&target_id, JobStatus::Processing, None);
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::BuildStarted { target_id } => {
                    display.update_job_status(&target_id, JobStatus::Processing, None);
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::InstallStarted { target_id, .. } => {
                    display.update_job_status(&target_id, JobStatus::Installing, None);
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::LinkStarted { target_id, .. } => {
                    display.update_job_status(&target_id, JobStatus::Linking, None);
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::JobSuccess {
                    target_id,
                    action,
                    pkg_type,
                } => {
                    display.update_job_status(&target_id, JobStatus::Success, None);
                    let type_str = match pkg_type {
                        PipelinePackageType::Formula => "Formula",
                        PipelinePackageType::Cask => "Cask",
                    };
                    let action_str = match action {
                        sps_common::pipeline::JobAction::Install => "Installed",
                        sps_common::pipeline::JobAction::Upgrade { .. } => "Upgraded",
                        sps_common::pipeline::JobAction::Reinstall { .. } => "Reinstalled",
                    };
                    logs_buffer.push(format!(
                        "{}: {} ({})",
                        action_str.green(),
                        target_id.cyan(),
                        type_str,
                    ));
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::JobFailed {
                    target_id, error, ..
                } => {
                    display.update_job_status(&target_id, JobStatus::Failed, None);
                    logs_buffer.push(format!(
                        "{} {}: {}",
                        "✗".red().bold(),
                        target_id.cyan(),
                        error.red()
                    ));
                    if pipeline_active {
                        display.render();
                    }
                }
                PipelineEvent::LogInfo { message } => {
                    logs_buffer.push(message);
                }
                PipelineEvent::LogWarn { message } => {
                    logs_buffer.push(message.yellow().to_string());
                }
                PipelineEvent::LogError { message } => {
                    logs_buffer.push(message.red().to_string());
                }
                PipelineEvent::PipelineFinished {
                    duration_secs,
                    success_count,
                    fail_count,
                } => {
                    if display.header_printed {
                        display.render();
                    }

                    println!();

                    println!(
                        "{} in {:.2}s ({} succeeded, {} failed)",
                        "Pipeline finished".bold(),
                        duration_secs,
                        success_count,
                        fail_count
                    );

                    if !logs_buffer.is_empty() {
                        println!();
                        for log in &logs_buffer {
                            println!("{log}");
                        }
                    }

                    break;
                }
                _ => {}
            },
            Err(broadcast::error::RecvError::Closed) => {
                break;
            }
            Err(broadcast::error::RecvError::Lagged(_)) => {
                // Ignore lag for now
            }
                }
            }
        }
    }
}

// sps-core/src/uninstall/formula.rs
use sps_common::config::Config;
use sps_common::error::{Result, SpsError};
use tracing::{debug, error, warn};

use crate::check::installed::InstalledPackageInfo;
use crate::install; // For install::bottle::link
use crate::uninstall::common::{remove_filesystem_artifact, UninstallOptions};

pub fn uninstall_formula_artifacts(
    info: &InstalledPackageInfo,
    config: &Config,
    _options: &UninstallOptions, /* options currently unused for formula but kept for signature
                                  * consistency */
) -> Result<()> {
    debug!(
        "Uninstalling Formula artifacts for {} version {}",
        info.name, info.version
    );

    // 1. Unlink artifacts
    // This function should handle removal of symlinks from /opt/sps/bin, /opt/sps/lib etc.
    // and the /opt/sps/opt/formula_name link.
    install::bottle::link::unlink_formula_artifacts(&info.name, &info.version, config)?;

    // 2. Remove the keg directory
    if info.path.exists() {
        debug!("Removing formula keg directory: {}", info.path.display());
        // For formula kegs, we generally expect them to be owned by the user or sps,
        // but sudo might be involved if permissions were changed manually or during a problematic
        // install. Setting use_sudo to true provides a fallback, though ideally it's not
        // needed for user-owned kegs.
        let use_sudo = true;
        if !remove_filesystem_artifact(&info.path, use_sudo) {
            // Check if it still exists after the removal attempt
            if info.path.exists() {
                error!(
                    "Failed remove keg {}: Check logs for sudo errors or other filesystem issues.",
                    info.path.display()
                );
                return Err(SpsError::InstallError(format!(
                    "Failed to remove keg directory: {}",
                    info.path.display()
                )));
            } else {
                // It means remove_filesystem_artifact returned false but the dir is gone
                // (possibly removed by sudo, or a race condition if another process removed it)
                debug!("Keg directory successfully removed (possibly with sudo).");
            }
        }
    } else {
        warn!(
            "Keg directory {} not found during uninstall. It might have been already removed.",
            info.path.display()
        );
    }
    Ok(())
}

// sps-core/src/upgrade/bottle.rs

use std::path::{Path, PathBuf};
use std::sync::Arc;

use sps_common::config::Config;
use sps_common::error::{Result as SpsResult, SpsError};
use sps_common::model::formula::Formula;
use tracing::{debug, error};

use crate::check::installed::InstalledPackageInfo;
use crate::{install, uninstall};

/// Upgrades a formula that is installed from a bottle.
///
/// This involves:
/// 1. Uninstalling the old version of the formula.
/// 2. Installing the new bottle.
/// 3. Linking the new version.
pub async fn upgrade_bottle_formula(
    formula: &Formula,
    new_bottle_download_path: &Path,
    old_install_info: &InstalledPackageInfo,
    config: &Config,
    http_client: Arc<reqwest::Client>, /* Added for download_bottle if needed, though path is
                                        * pre-downloaded */
) -> SpsResult<PathBuf> {
    debug!(
        "Upgrading bottle formula {} from {} to {}",
        formula.name(),
        old_install_info.version,
        formula.version_str_full()
    );

    // 1. Uninstall the old version
    debug!(
        "Uninstalling old bottle version: {} at {}",
        old_install_info.version,
        old_install_info.path.display()
    );
    let uninstall_opts = uninstall::UninstallOptions { skip_zap: true }; // Zap is not relevant for formula upgrades
    uninstall::formula::uninstall_formula_artifacts(old_install_info, config, &uninstall_opts)
        .map_err(|e| {
            error!(
                "Failed to uninstall old version {} of formula {}: {}",
                old_install_info.version,
                formula.name(),
                e
            );
            SpsError::InstallError(format!(
                "Failed to uninstall old version during upgrade of {}: {e}",
                formula.name()
            ))
        })?;
    debug!("Successfully uninstalled old version of {}", formula.name());

    // 2. Install the new bottle
    // The new_bottle_download_path is already provided, so we call install_bottle directly.
    // If download was part of this function, http_client would be used.
    let _ = http_client; // Mark as used if not directly needed by install_bottle

    debug!(
        "Installing new bottle for {} from {}",
        formula.name(),
        new_bottle_download_path.display()
    );
    let installed_keg_path =
        install::bottle::exec::install_bottle(new_bottle_download_path, formula, config).map_err(
            |e| {
                error!(
                    "Failed to install new bottle for formula {}: {}",
                    formula.name(),
                    e
                );
                SpsError::InstallError(format!(
                    "Failed to install new bottle during upgrade of {}: {e}",
                    formula.name()
                ))
            },
        )?;
    debug!(
        "Successfully installed new bottle for {} to {}",
        formula.name(),
        installed_keg_path.display()
    );

    // 3. Link the new version (linking is handled by the worker after this function returns the
    //    path)
    // The install::bottle::exec::install_bottle writes the receipt, but linking is separate.
    // The worker will call link_formula_artifacts after this.

    Ok(installed_keg_path)
}

// src/utils/cache.rs
// Handles caching of formula data and downloads

use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};

use super::error::{Result, SpsError};
use crate::Config;

/// Define how long cache entries are considered valid
const CACHE_TTL: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours

/// Cache struct to manage cache operations
pub struct Cache {
    cache_dir: PathBuf,
    _config: Config, // Keep a reference to config if needed for other paths or future use
}

impl Cache {
    /// Create a new Cache using the config's cache_dir
    pub fn new(config: &Config) -> Result<Self> {
        let cache_dir = config.cache_dir();
        if !cache_dir.exists() {
            fs::create_dir_all(&cache_dir)?;
        }

        Ok(Self {
            cache_dir,
            _config: config.clone(),
        })
    }

    /// Gets the cache directory path
    pub fn get_dir(&self) -> &Path {
        &self.cache_dir
    }

    /// Stores raw string data in the cache
    pub fn store_raw(&self, filename: &str, data: &str) -> Result<()> {
        let path = self.cache_dir.join(filename);
        tracing::debug!("Saving raw data to cache file: {:?}", path);
        fs::write(&path, data)?;
        Ok(())
    }

    /// Loads raw string data from the cache
    pub fn load_raw(&self, filename: &str) -> Result<String> {
        let path = self.cache_dir.join(filename);
        tracing::debug!("Loading raw data from cache file: {:?}", path);

        if !path.exists() {
            return Err(SpsError::Cache(format!(
                "Cache file {filename} does not exist"
            )));
        }

        fs::read_to_string(&path).map_err(|e| SpsError::Cache(format!("IO error: {e}")))
    }

    /// Checks if a cache file exists and is valid (within TTL)
    pub fn is_cache_valid(&self, filename: &str) -> Result<bool> {
        let path = self.cache_dir.join(filename);
        if !path.exists() {
            return Ok(false);
        }

        let metadata = fs::metadata(&path)?;
        let modified_time = metadata.modified()?;
        let age = SystemTime::now()
            .duration_since(modified_time)
            .map_err(|e| SpsError::Cache(format!("System time error: {e}")))?;

        Ok(age <= CACHE_TTL)
    }

    /// Clears a specific cache file
    pub fn clear_file(&self, filename: &str) -> Result<()> {
        let path = self.cache_dir.join(filename);
        if path.exists() {
            fs::remove_file(&path)?;
        }
        Ok(())
    }

    /// Clears all cache files
    pub fn clear_all(&self) -> Result<()> {
        if self.cache_dir.exists() {
            fs::remove_dir_all(&self.cache_dir)?;
            fs::create_dir_all(&self.cache_dir)?;
        }
        Ok(())
    }

    /// Gets a reference to the config
    pub fn config(&self) -> &Config {
        &self._config
    }
}

// tap/tap.rs - Basic tap functionality // Should probably be in model module

use std::path::PathBuf;

use tracing::debug;

use crate::error::{Result, SpsError};

/// Represents a source of packages (formulas and casks)
pub struct Tap {
    /// The user part of the tap name (e.g., "homebrew" in "homebrew/core")
    pub user: String,

    /// The repository part of the tap name (e.g., "core" in "homebrew/core")
    pub repo: String,

    /// The full path to the tap directory
    pub path: PathBuf,
}

impl Tap {
    /// Create a new tap from user/repo format
    pub fn new(name: &str) -> Result<Self> {
        let parts: Vec<&str> = name.split('/').collect();
        if parts.len() != 2 {
            return Err(SpsError::Generic(format!("Invalid tap name: {name}")));
        }
        let user = parts[0].to_string();
        let repo = parts[1].to_string();
        let prefix = if cfg!(target_arch = "aarch64") {
            PathBuf::from("/opt/homebrew")
        } else {
            PathBuf::from("/usr/local")
        };
        let path = prefix
            .join("Library/Taps")
            .join(&user)
            .join(format!("homebrew-{repo}"));
        Ok(Self { user, repo, path })
    }

    /// Update this tap by pulling latest changes
    pub fn update(&self) -> Result<()> {
        use git2::{FetchOptions, Repository};

        let repo = Repository::open(&self.path)
            .map_err(|e| SpsError::Generic(format!("Failed to open tap repository: {e}")))?;

        // Fetch updates from origin
        let mut remote = repo
            .find_remote("origin")
            .map_err(|e| SpsError::Generic(format!("Failed to find remote 'origin': {e}")))?;

        let mut fetch_options = FetchOptions::new();
        remote
            .fetch(
                &["refs/heads/*:refs/heads/*"],
                Some(&mut fetch_options),
                None,
            )
            .map_err(|e| SpsError::Generic(format!("Failed to fetch updates: {e}")))?;

        // Merge changes
        let fetch_head = repo
            .find_reference("FETCH_HEAD")
            .map_err(|e| SpsError::Generic(format!("Failed to find FETCH_HEAD: {e}")))?;

        let fetch_commit = repo
            .reference_to_annotated_commit(&fetch_head)
            .map_err(|e| SpsError::Generic(format!("Failed to get commit from FETCH_HEAD: {e}")))?;

        let analysis = repo
            .merge_analysis(&[&fetch_commit])
            .map_err(|e| SpsError::Generic(format!("Failed to analyze merge: {e}")))?;

        if analysis.0.is_up_to_date() {
            debug!("Already up-to-date");
            return Ok(());
        }

        if analysis.0.is_fast_forward() {
            let mut reference = repo
                .find_reference("refs/heads/master")
                .map_err(|e| SpsError::Generic(format!("Failed to find master branch: {e}")))?;
            reference
                .set_target(fetch_commit.id(), "Fast-forward")
                .map_err(|e| SpsError::Generic(format!("Failed to fast-forward: {e}")))?;
            repo.set_head("refs/heads/master")
                .map_err(|e| SpsError::Generic(format!("Failed to set HEAD: {e}")))?;
            repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
                .map_err(|e| SpsError::Generic(format!("Failed to checkout: {e}")))?;
        } else {
            return Err(SpsError::Generic(
                "Tap requires merge but automatic merging is not implemented".to_string(),
            ));
        }

        Ok(())
    }

    /// Remove this tap by deleting its local repository
    pub fn remove(&self) -> Result<()> {
        if !self.path.exists() {
            return Err(SpsError::NotFound(format!(
                "Tap {} is not installed",
                self.full_name()
            )));
        }
        debug!("Removing tap {}", self.full_name());
        std::fs::remove_dir_all(&self.path).map_err(|e| {
            SpsError::Generic(format!("Failed to remove tap {}: {}", self.full_name(), e))
        })
    }

    /// Get the full name of the tap (user/repo)
    pub fn full_name(&self) -> String {
        format!("{}/{}", self.user, self.repo)
    }

    /// Check if this tap is installed locally
    pub fn is_installed(&self) -> bool {
        self.path.exists()
    }
}

// sps-common/src/model/artifact.rs
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// Represents an item installed or managed by sps, recorded in the manifest.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] // Added Hash
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InstalledArtifact {
    /// The main application bundle (e.g., in /Applications).
    AppBundle { path: PathBuf },
    /// A command-line binary symlinked into the prefix's bin dir.
    BinaryLink {
        link_path: PathBuf,
        target_path: PathBuf,
    },
    /// A man page symlinked into the prefix's man dir.
    ManpageLink {
        link_path: PathBuf,
        target_path: PathBuf,
    },
    /// A resource moved to a standard system/user location (e.g., Font, PrefPane).
    MovedResource { path: PathBuf },
    /// A macOS package receipt ID managed by pkgutil.
    PkgUtilReceipt { id: String },
    /// A launchd service (Agent/Daemon).
    Launchd {
        label: String,
        path: Option<PathBuf>,
    }, // Path is the plist file
    /// A symlink created within the Caskroom pointing to the actual installed artifact.
    /// Primarily for internal reference and potentially easier cleanup if needed.
    CaskroomLink {
        link_path: PathBuf,
        target_path: PathBuf,
    },
    /// A file copied *into* the Caskroom (e.g., a .pkg installer).
    CaskroomReference { path: PathBuf },
}

// Optional: Helper methods if needed
// impl InstalledArtifact { ... }

// sps-core/src/upgrade/source.rs

use std::path::{Path, PathBuf};

use sps_common::config::Config;
use sps_common::error::{Result as SpsResult, SpsError};
use sps_common::model::formula::Formula;
use tracing::{debug, error};

use crate::check::installed::InstalledPackageInfo;
use crate::{build, uninstall};

/// Upgrades a formula that was/will be installed from source.
///
/// This involves:
/// 1. Uninstalling the old version of the formula.
/// 2. Building and installing the new version from source.
/// 3. Linking the new version.
pub async fn upgrade_source_formula(
    formula: &Formula,
    new_source_download_path: &Path,
    old_install_info: &InstalledPackageInfo,
    config: &Config,
    all_installed_dependency_paths: &[PathBuf], // For build environment
) -> SpsResult<PathBuf> {
    debug!(
        "Upgrading source-built formula {} from {} to {}",
        formula.name(),
        old_install_info.version,
        formula.version_str_full()
    );

    // 1. Uninstall the old version
    debug!(
        "Uninstalling old source-built version: {} at {}",
        old_install_info.version,
        old_install_info.path.display()
    );
    let uninstall_opts = uninstall::UninstallOptions { skip_zap: true };
    uninstall::formula::uninstall_formula_artifacts(old_install_info, config, &uninstall_opts)
        .map_err(|e| {
            error!(
                "Failed to uninstall old version {} of formula {}: {}",
                old_install_info.version,
                formula.name(),
                e
            );
            SpsError::InstallError(format!(
                "Failed to uninstall old version during source upgrade of {}: {e}",
                formula.name()
            ))
        })?;
    debug!(
        "Successfully uninstalled old source-built version of {}",
        formula.name()
    );

    // 2. Build and install the new version from source
    debug!(
        "Building new version of {} from source path {}",
        formula.name(),
        new_source_download_path.display()
    );
    let installed_keg_path = build::compile::build_from_source(
        new_source_download_path,
        formula,
        config,
        all_installed_dependency_paths,
    )
    .await
    .map_err(|e| {
        error!(
            "Failed to build new version of formula {} from source: {}",
            formula.name(),
            e
        );
        SpsError::InstallError(format!(
            "Failed to build new version from source during upgrade of {}: {e}",
            formula.name()
        ))
    })?;
    debug!(
        "Successfully built and installed new version of {} to {}",
        formula.name(),
        installed_keg_path.display()
    );

    // 3. Linking is handled by the worker after this function returns the path.

    Ok(installed_keg_path)
}

// sps-io/src/checksum.rs
//use std::sync::Arc;
use std::fs::File;
use std::io;
use std::path::Path;

use infer;
use sha2::{Digest, Sha256};
use sps_common::error::{Result, SpsError};
use url::Url;
//use tokio::fs::File;
//use tokio::io::AsyncReadExt;
//use tracing::debug; // Use tracing

///// Asynchronously verifies the SHA256 checksum of a file.
///// Reads the file asynchronously but performs hashing synchronously.
//pub async fn verify_checksum_async(path: &Path, expected: &str) -> Result<()> {
//debug!("Async Verifying checksum for: {}", path.display());
//    let file = File::open(path).await;
//    let mut file = match file {
//        Ok(f) => f,
//        Err(e) => {
//            return Err(SpsError::Io(Arc::new(e))); // Wrap IO error
//        }
//    };
//
//    let mut hasher = Sha256::new();
//    let mut buffer = Vec::with_capacity(8192); // Use a Vec as buffer for read_buf
//    let mut total_bytes_read = 0;
//
//    loop {
//        buffer.clear();
//        match file.read_buf(&mut buffer).await {
//            Ok(0) => break, // End of file
//            Ok(n) => {
//                hasher.update(&buffer[..n]);
//                total_bytes_read += n as u64;
//            }
//            Err(e) => {
//                return Err(SpsError::Io(Arc::new(e))); // Wrap IO error
//            }
//        }
//    }
//
//    let hash_bytes = hasher.finalize();
//    let actual = hex::encode(hash_bytes);
//
//    debug!(
//        "Async Calculated SHA256: {} ({} bytes read)",
//        actual, total_bytes_read
//    );
//    debug!("Expected SHA256:   {}", expected);
//
//    if actual.eq_ignore_ascii_case(expected) {
//        Ok(())
//    } else {
//        Err(SpsError::ChecksumError(format!(
//            "Checksum mismatch for {}: expected {}, got {}",
//            path.display(),
//            expected,
//            actual
//        )))
//    }
//}

// Keep the synchronous version for now if needed elsewhere or for comparison
pub fn verify_checksum(path: &Path, expected: &str) -> Result<()> {
    tracing::debug!("Verifying checksum for: {}", path.display());
    let mut file = File::open(path)?;
    let mut hasher = Sha256::new();
    let bytes_copied = io::copy(&mut file, &mut hasher)?;
    let hash_bytes = hasher.finalize();
    let actual = hex::encode(hash_bytes);
    tracing::debug!(
        "Calculated SHA256: {} ({} bytes read)",
        actual,
        bytes_copied
    );
    tracing::debug!("Expected SHA256:   {}", expected);
    if actual.eq_ignore_ascii_case(expected) {
        Ok(())
    } else {
        Err(SpsError::ChecksumError(format!(
            "Checksum mismatch for {}: expected {}, got {}",
            path.display(),
            expected,
            actual
        )))
    }
}

/// Verifies that the detected content type of the file matches the expected extension.
pub fn verify_content_type(path: &Path, expected_ext: &str) -> Result<()> {
    let kind_opt = infer::get_from_path(path)?;
    if let Some(kind) = kind_opt {
        let actual_ext = kind.extension();
        if actual_ext.eq_ignore_ascii_case(expected_ext) {
            tracing::debug!(
                "Content type verified: {} matches expected {}",
                actual_ext,
                expected_ext
            );
            Ok(())
        } else {
            Err(SpsError::Generic(format!(
                "Content type mismatch for {}: expected extension '{}', but detected '{}'",
                path.display(),
                expected_ext,
                actual_ext
            )))
        }
    } else {
        Err(SpsError::Generic(format!(
            "Could not determine content type for {}",
            path.display()
        )))
    }
}

/// Validates a URL, ensuring it uses the HTTPS scheme.
pub fn validate_url(url_str: &str) -> Result<()> {
    let url = Url::parse(url_str)
        .map_err(|e| SpsError::Generic(format!("Failed to parse URL '{url_str}': {e}")))?;
    if url.scheme() == "https" {
        Ok(())
    } else {
        Err(SpsError::ValidationError(format!(
            "Invalid URL scheme for '{}': Must be https, but got '{}'",
            url_str,
            url.scheme()
        )))
    }
}

// sps-cli/src/cli/install.rs

use std::sync::Arc;

use clap::Args;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result;
use tracing::instrument;

// Import pipeline components from the new module
use crate::pipeline::runner::{self, CommandType, PipelineFlags};

// Keep the Args struct specific to 'install' if needed, or reuse a common one
#[derive(Debug, Args)]
pub struct InstallArgs {
    #[arg(required = true)]
    names: Vec<String>,

    // Keep flags relevant to install/pipeline
    #[arg(long)]
    skip_deps: bool, // Note: May not be fully supported by core resolution yet
    #[arg(long, help = "Force install specified targets as casks")]
    cask: bool,
    #[arg(long, help = "Force install specified targets as formulas")]
    formula: bool,
    #[arg(long)]
    include_optional: bool,
    #[arg(long)]
    skip_recommended: bool,
    #[arg(
        long,
        help = "Force building the formula from source, even if a bottle is available"
    )]
    build_from_source: bool,
    // Worker/Queue size flags might belong here or be global CLI flags
    // #[arg(long, value_name = "sps_WORKERS")]
    // max_workers: Option<usize>,
    // #[arg(long, value_name = "sps_QUEUE")]
    // queue_size: Option<usize>,
}

impl InstallArgs {
    #[instrument(skip(self, config, cache), fields(targets = ?self.names))]
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        // --- Argument Validation (moved from old run) ---
        if self.formula && self.cask {
            return Err(sps_common::error::SpsError::Generic(
                "Cannot use --formula and --cask together.".to_string(),
            ));
        }
        // Add validation for skip_deps if needed

        // --- Prepare Pipeline Flags ---
        let flags = PipelineFlags {
            build_from_source: self.build_from_source,
            include_optional: self.include_optional,
            skip_recommended: self.skip_recommended,
            // Add other flags...
        };

        // --- Determine Initial Targets based on --formula/--cask flags ---
        // (This logic might be better inside plan_package_operations based on CommandType)
        let initial_targets = self.names.clone(); // For install, all names are initial targets

        // --- Execute the Pipeline ---
        runner::run_pipeline(
            &initial_targets,
            CommandType::Install, // Specify the command type
            config,
            cache,
            &flags, // Pass the flags struct
        )
        .await
    }
}

use std::collections::HashMap;
use std::sync::Arc;

use tracing::debug;

use super::cache::Cache;
use super::config::Config;
use super::error::{Result, SpsError};
use super::model::formula::Formula;

#[derive()]
pub struct Formulary {
    cache: Cache,
    parsed_cache: std::sync::Mutex<HashMap<String, std::sync::Arc<Formula>>>,
}

impl Formulary {
    pub fn new(config: Config) -> Self {
        let cache = Cache::new(&config).unwrap_or_else(|e| {
            panic!("Failed to initialize cache in Formulary: {e}");
        });
        Self {
            cache,
            parsed_cache: std::sync::Mutex::new(HashMap::new()),
        }
    }

    pub fn load_formula(&self, name: &str) -> Result<Formula> {
        let mut parsed_cache_guard = self.parsed_cache.lock().unwrap();
        if let Some(formula_arc) = parsed_cache_guard.get(name) {
            debug!("Loaded formula '{}' from parsed cache.", name);
            return Ok(Arc::clone(formula_arc).as_ref().clone());
        }
        drop(parsed_cache_guard);

        let raw_data = self.cache.load_raw("formula.json")?;
        let all_formulas: Vec<Formula> = serde_json::from_str(&raw_data)
            .map_err(|e| SpsError::Cache(format!("Failed to parse cached formula data: {e}")))?;
        debug!("Parsed {} formulas.", all_formulas.len());

        let mut found_formula: Option<Formula> = None;
        parsed_cache_guard = self.parsed_cache.lock().unwrap();
        for formula in all_formulas {
            let formula_name = formula.name.clone();
            let formula_arc = std::sync::Arc::new(formula);

            if formula_name == name {
                found_formula = Some(Arc::clone(&formula_arc).as_ref().clone());
            }

            parsed_cache_guard
                .entry(formula_name)
                .or_insert(formula_arc);
        }

        match found_formula {
            Some(f) => {
                debug!(
                    "Successfully loaded formula '{}' version {}",
                    f.name,
                    f.version_str_full()
                );
                Ok(f)
            }
            None => {
                debug!(
                    "Formula '{}' not found within the cached formula data.",
                    name
                );
                Err(SpsError::Generic(format!(
                    "Formula '{name}' not found in cache."
                )))
            }
        }
    }
}

//! Contains the logic for the `update` command.
use std::fs;
use std::sync::Arc;

use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result;
use sps_net::api;

#[derive(clap::Args, Debug)]
pub struct Update;

impl Update {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        tracing::debug!("Running manual update..."); // Log clearly it's the manual one

        // Use the ui utility function to create the spinner
        println!("Updating package lists"); // <-- CHANGED

        tracing::debug!("Using cache directory: {:?}", config.cache_dir());

        // Fetch and store raw formula data
        match api::fetch_all_formulas().await {
            Ok(raw_data) => {
                cache.store_raw("formula.json", &raw_data)?;
                tracing::debug!("✓ Successfully cached formulas data");
                println!("Cached formulas data");
            }
            Err(e) => {
                let err_msg = format!("Failed to fetch/store formulas from API: {e}");
                tracing::error!("{}", err_msg);
                println!(); // Clear spinner on error
                return Err(e);
            }
        }

        // Fetch and store raw cask data
        match api::fetch_all_casks().await {
            Ok(raw_data) => {
                cache.store_raw("cask.json", &raw_data)?;
                tracing::debug!("✓ Successfully cached casks data");
                println!("Cached casks data");
            }
            Err(e) => {
                let err_msg = format!("Failed to fetch/store casks from API: {e}");
                tracing::error!("{}", err_msg);
                return Err(e);
            }
        }

        // Update timestamp file
        let timestamp_file = config.state_dir().join(".sps_last_update_check");
        tracing::debug!(
            "Manual update successful. Updating timestamp file: {}",
            timestamp_file.display()
        );
        match fs::File::create(&timestamp_file) {
            Ok(_) => {
                tracing::debug!("Updated timestamp file successfully.");
            }
            Err(e) => {
                tracing::warn!(
                    "Failed to create or update timestamp file '{}': {}",
                    timestamp_file.display(),
                    e
                );
            }
        }

        println!("Update completed successfully!");
        Ok(())
    }
}

// **File:** sps-core/src/model/version.rs (New file)
use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::error::{Result, SpsError};

/// Wrapper around semver::Version for formula versions.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Version(semver::Version);

impl Version {
    pub fn parse(s: &str) -> Result<Self> {
        // Attempt standard semver parse first
        semver::Version::parse(s).map(Version).or_else(|_| {
            // Homebrew often uses versions like "1.2.3_1" (revision) or just "123"
            // Try to handle these by stripping suffixes or padding
            // This is a simplified handling, Homebrew's PkgVersion is complex
            let cleaned = s.split('_').next().unwrap_or(s); // Take part before _
            let parts: Vec<&str> = cleaned.split('.').collect();
            let padded = match parts.len() {
                1 => format!("{}.0.0", parts[0]),
                2 => format!("{}.{}.0", parts[0], parts[1]),
                _ => cleaned.to_string(), // Use original if 3+ parts
            };
            semver::Version::parse(&padded).map(Version).map_err(|e| {
                SpsError::VersionError(format!(
                    "Failed to parse version '{s}' (tried '{padded}'): {e}"
                ))
            })
        })
    }
}

impl FromStr for Version {
    type Err = SpsError;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // TODO: Preserve original format if possible? PkgVersion complexity.
        // For now, display the parsed semver representation.
        write!(f, "{}", self.0)
    }
}

// Manual Serialize/Deserialize to handle the Version<->String conversion
impl Serialize for Version {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl AsRef<Self> for Version {
    fn as_ref(&self) -> &Self {
        self
    }
}

// Removed redundant ToString implementation as it conflicts with the blanket implementation in std.

impl From<Version> for semver::Version {
    fn from(version: Version) -> Self {
        version.0
    }
}

impl<'de> Deserialize<'de> for Version {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::from_str(&s).map_err(serde::de::Error::custom)
    }
}

// Add to sps-core/src/utils/error.rs:
// #[error("Version error: {0}")]
// VersionError(String),

// Add to sps-core/Cargo.toml:
// [dependencies]
// semver = "1.0"

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

use crossbeam_channel::Receiver as CrossbeamReceiver;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result as SpsResult;
use sps_common::pipeline::{PipelineEvent, WorkerJob};
use threadpool::ThreadPool;
use tokio::sync::broadcast;
use tracing::{debug, instrument};

use super::worker;

#[instrument(skip_all, name = "core_worker_manager")]
pub fn start_worker_pool_manager(
    config: Config,
    cache: Arc<Cache>,
    worker_job_rx: CrossbeamReceiver<WorkerJob>,
    event_tx: broadcast::Sender<PipelineEvent>,
    success_count: Arc<AtomicUsize>,
    fail_count: Arc<AtomicUsize>,
) -> SpsResult<()> {
    let num_workers = std::cmp::max(1, num_cpus::get_physical().saturating_sub(1)).min(6);
    let pool = ThreadPool::new(num_workers);
    debug!(
        "Core worker pool manager started with {} workers.",
        num_workers
    );
    debug!("Worker pool created.");

    for worker_job in worker_job_rx {
        let job_id = worker_job.request.target_id.clone();
        debug!(
            "[{}] Received job from channel, preparing to submit to pool.",
            job_id
        );

        let config_clone = config.clone();
        let cache_clone = Arc::clone(&cache);
        let event_tx_clone = event_tx.clone();
        let success_count_clone = Arc::clone(&success_count);
        let fail_count_clone = Arc::clone(&fail_count);

        let _ = event_tx_clone.send(PipelineEvent::JobProcessingStarted {
            target_id: job_id.clone(),
        });

        debug!("[{}] Submitting job to worker pool.", job_id);

        pool.execute(move || {
            let job_result = worker::execute_sync_job(
                worker_job,
                &config_clone,
                cache_clone,
                event_tx_clone.clone(),
            );
            let job_id_for_log = job_id.clone();
            debug!(
                "[{}] Worker job execution finished (execute_sync_job returned), result ok: {}",
                job_id_for_log,
                job_result.is_ok()
            );

            let job_id_for_log = job_id.clone();
            match job_result {
                Ok((final_action, pkg_type)) => {
                    success_count_clone.fetch_add(1, Ordering::Relaxed);
                    debug!("[{}] Worker finished successfully.", job_id);
                    let _ = event_tx_clone.send(PipelineEvent::JobSuccess {
                        target_id: job_id,
                        action: final_action,
                        pkg_type,
                    });
                    debug!("[{}] Worker result event sent.", job_id_for_log);
                }
                Err(boxed) => {
                    let (final_action, err) = *boxed;
                    fail_count_clone.fetch_add(1, Ordering::Relaxed);
                    // Error is sent via JobFailed event and displayed in status.rs
                    let _ = event_tx_clone.send(PipelineEvent::JobFailed {
                        target_id: job_id,
                        action: final_action,
                        error: err.to_string(),
                    });
                    debug!("[{}] Worker result event sent.", job_id_for_log);
                }
            }
            debug!("[{}] Worker closure scope ending.", job_id_for_log);
        });
    }
    pool.join();
    Ok(())
}

use std::sync::Arc;

use clap::Args;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result;
use sps_core::check::installed;

use crate::pipeline::runner::{self, CommandType, PipelineFlags};

#[derive(Args, Debug)]
pub struct UpgradeArgs {
    #[arg()]
    pub names: Vec<String>,

    #[arg(long, conflicts_with = "names")]
    pub all: bool,

    #[arg(long)]
    pub build_from_source: bool,
}

impl UpgradeArgs {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        let targets = if self.all {
            // Get all installed package names
            let installed = installed::get_installed_packages(config).await?;
            installed.into_iter().map(|p| p.name).collect()
        } else {
            self.names.clone()
        };

        if targets.is_empty() {
            return Ok(());
        }

        let flags = PipelineFlags {
            // Populate flags from args
            build_from_source: self.build_from_source,
            // Upgrade should respect original install options ideally,
            // but for now let's default them. This could be enhanced later
            // by reading install receipts.
            include_optional: false,
            skip_recommended: false,
            // ... add other common flags if needed ...
        };

        runner::run_pipeline(
            &targets,
            CommandType::Upgrade { all: self.all },
            config,
            cache,
            &flags,
        )
        .await
    }
}

// sps/src/cli.rs
//! Defines the command-line argument structure using clap.
use std::sync::Arc;

use clap::{ArgAction, Parser, Subcommand};
use sps_common::error::Result;
use sps_common::{Cache, Config};

// Module declarations
pub mod info;
pub mod init;
pub mod install;
pub mod list;
pub mod reinstall;
pub mod search;
pub mod status;
pub mod uninstall;
pub mod update;
pub mod upgrade;
// Re-export InitArgs to make it accessible as cli::InitArgs
// Import other command Args structs
use crate::cli::info::Info;
pub use crate::cli::init::InitArgs;
use crate::cli::install::InstallArgs;
use crate::cli::list::List;
use crate::cli::reinstall::ReinstallArgs;
use crate::cli::search::Search;
use crate::cli::uninstall::Uninstall;
use crate::cli::update::Update;
use crate::cli::upgrade::UpgradeArgs;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None, name = "sps", bin_name = "sps")]
#[command(propagate_version = true)]
pub struct CliArgs {
    #[arg(short, long, action = ArgAction::Count, global = true)]
    pub verbose: u8,

    #[command(subcommand)]
    pub command: Command,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    Init(InitArgs),
    Search(Search),
    List(List),
    Info(Info),
    Update(Update),
    Install(InstallArgs),
    Uninstall(Uninstall),
    Reinstall(ReinstallArgs),
    Upgrade(UpgradeArgs),
}

impl Command {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        match self {
            Self::Init(command) => command.run(config).await,
            Self::Search(command) => command.run(config, cache).await,
            Self::List(command) => command.run(config, cache).await,
            Self::Info(command) => command.run(config, cache).await,
            Self::Update(command) => command.run(config, cache).await,
            // Commands that use the pipeline
            Self::Install(command) => command.run(config, cache).await,
            Self::Reinstall(command) => command.run(config, cache).await,
            Self::Upgrade(command) => command.run(config, cache).await,
            Self::Uninstall(command) => command.run(config, cache).await,
        }
    }
}

// In install.rs, reinstall.rs, upgrade.rs, their run methods will now call
// sps::cli::pipeline_runner::run_pipeline(...)
// e.g., in sps/src/cli/install.rs:
// use crate::cli::pipeline_runner::{self, CommandType, PipelineFlags};
// ...
// pipeline_runner::run_pipeline(&initial_targets, CommandType::Install, config, cache,
// &flags).await

// sps-common/src/pipeline.rs
use std::path::PathBuf;
use std::sync::Arc; // Required for Arc<SpsError> in JobProcessingState

use serde::{Deserialize, Serialize};

use crate::dependency::ResolvedGraph; // Needed for planner output
use crate::error::SpsError;
use crate::model::InstallTargetIdentifier;

// --- Shared Enums / Structs ---

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PipelinePackageType {
    Formula,
    Cask,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] // Added PartialEq, Eq
pub enum JobAction {
    Install,
    Upgrade {
        from_version: String,
        old_install_path: PathBuf,
    },
    Reinstall {
        version: String,
        current_install_path: PathBuf,
    },
}

#[derive(Debug, Clone)]
pub struct PlannedJob {
    pub target_id: String,
    pub target_definition: InstallTargetIdentifier,
    pub action: JobAction,
    pub is_source_build: bool,
    pub use_private_store_source: Option<PathBuf>,
}

#[derive(Debug, Clone)]
pub struct WorkerJob {
    pub request: PlannedJob,
    pub download_path: PathBuf,
    pub download_size_bytes: u64,
    pub is_source_from_private_store: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PipelineEvent {
    PipelineStarted {
        total_jobs: usize,
    },
    PipelineFinished {
        duration_secs: f64,
        success_count: usize,
        fail_count: usize,
    },
    PlanningStarted,
    DependencyResolutionStarted,
    DependencyResolutionFinished,
    PlanningFinished {
        job_count: usize,
        // Optionally, we can pass the ResolvedGraph here if the status handler needs it,
        // but it might be too large for a broadcast event.
        // resolved_graph: Option<Arc<ResolvedGraph>>, // Example
    },
    DownloadStarted {
        target_id: String,
        url: String,
    },
    DownloadFinished {
        target_id: String,
        path: PathBuf,
        size_bytes: u64,
    },
    DownloadProgressUpdate {
        target_id: String,
        bytes_so_far: u64,
        total_size: Option<u64>,
    },
    DownloadCached {
        target_id: String,
        size_bytes: u64,
    },
    DownloadFailed {
        target_id: String,
        url: String,
        error: String, // Keep as String for simplicity in events
    },
    JobProcessingStarted {
        // From core worker
        target_id: String,
    },
    JobDispatchedToCore {
        // New: From runner to UI when job sent to worker pool
        target_id: String,
    },
    UninstallStarted {
        target_id: String,
        version: String,
    },
    UninstallFinished {
        target_id: String,
        version: String,
    },
    BuildStarted {
        target_id: String,
    },
    InstallStarted {
        target_id: String,
        pkg_type: PipelinePackageType,
    },
    LinkStarted {
        target_id: String,
        pkg_type: PipelinePackageType,
    },
    JobSuccess {
        // From core worker
        target_id: String,
        action: JobAction,
        pkg_type: PipelinePackageType,
    },
    JobFailed {
        // From core worker or runner (propagated)
        target_id: String,
        action: JobAction, // Action that was attempted
        error: String,     // Keep as String
    },
    LogInfo {
        message: String,
    },
    LogWarn {
        message: String,
    },
    LogError {
        message: String,
    },
}

impl PipelineEvent {
    // SpsError kept for internal use, but events use String for error messages
    pub fn job_failed(target_id: String, action: JobAction, error: &SpsError) -> Self {
        PipelineEvent::JobFailed {
            target_id,
            action,
            error: error.to_string(),
        }
    }
    pub fn download_failed(target_id: String, url: String, error: &SpsError) -> Self {
        PipelineEvent::DownloadFailed {
            target_id,
            url,
            error: error.to_string(),
        }
    }
}

// --- New Structs and Enums for Refactored Runner ---

/// Represents the current processing state of a job in the pipeline.
#[derive(Debug, Clone)]
pub enum JobProcessingState {
    /// Waiting for download to be initiated.
    PendingDownload,
    /// Download is in progress (managed by DownloadCoordinator).
    Downloading,
    /// Download completed successfully, artifact at PathBuf.
    Downloaded(PathBuf),
    /// Downloaded, but waiting for dependencies to be in Succeeded state.
    WaitingForDependencies(PathBuf),
    /// Dispatched to the core worker pool for installation/processing.
    DispatchedToCore(PathBuf),
    /// Installation/processing is in progress by a core worker.
    Installing(PathBuf), // Path is still relevant
    /// Job completed successfully.
    Succeeded,
    /// Job failed. The String contains the error message. Arc for cheap cloning.
    Failed(Arc<SpsError>),
}

/// Outcome of a download attempt, sent from DownloadCoordinator to the main runner loop.
#[derive(Debug)] // Clone not strictly needed if moved
pub struct DownloadOutcome {
    pub planned_job: PlannedJob,           // The job this download was for
    pub result: Result<PathBuf, SpsError>, // Path to downloaded file or error
}

/// Structure returned by the planner, now including the ResolvedGraph.
#[derive(Debug, Default)]
pub struct PlannedOperations {
    pub jobs: Vec<PlannedJob>,           // Topologically sorted for formulae
    pub errors: Vec<(String, SpsError)>, // Errors from planning phase
    pub already_installed_or_up_to_date: std::collections::HashSet<String>,
    pub resolved_graph: Option<Arc<ResolvedGraph>>, // Graph for dependency checking in runner
}

// sps-cli/src/cli/reinstall.rs
use std::sync::Arc;

use clap::Args;
use sps_common::cache::Cache;
use sps_common::config::Config;
use sps_common::error::Result;

use crate::pipeline::runner::{self, CommandType, PipelineFlags};

#[derive(Args, Debug)]
pub struct ReinstallArgs {
    #[arg(required = true)]
    pub names: Vec<String>,

    #[arg(
        long,
        help = "Force building the formula from source, even if a bottle is available"
    )]
    pub build_from_source: bool,
}

impl ReinstallArgs {
    pub async fn run(&self, config: &Config, cache: Arc<Cache>) -> Result<()> {
        let flags = PipelineFlags {
            // Populate flags from args
            build_from_source: self.build_from_source,
            include_optional: false, // Reinstall usually doesn't change optional deps
            skip_recommended: true,  /* Reinstall usually doesn't change recommended deps
                                      * ... add other common flags if needed ... */
        };
        runner::run_pipeline(&self.names, CommandType::Reinstall, config, cache, &flags).await
    }
}

// **File:** sps-core/src/dependency/dependency.rs // Should be in the model module
use std::fmt;

use bitflags::bitflags;
use serde::{Deserialize, Serialize};

bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
    pub struct DependencyTag: u8 {
        const RUNTIME     = 0b00000001;
        const BUILD       = 0b00000010;
        const TEST        = 0b00000100;
        const OPTIONAL    = 0b00001000;
        const RECOMMENDED = 0b00010000;
    }
}

impl Default for DependencyTag {
    fn default() -> Self {
        Self::RUNTIME
    }
}

impl fmt::Display for DependencyTag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self:?}")
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Dependency {
    pub name: String,
    #[serde(default)]
    pub tags: DependencyTag,
}

impl Dependency {
    pub fn new_runtime(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            tags: DependencyTag::RUNTIME,
        }
    }

    pub fn new_with_tags(name: impl Into<String>, tags: DependencyTag) -> Self {
        Self {
            name: name.into(),
            tags,
        }
    }
}

pub trait DependencyExt {
    fn filter_by_tags(&self, include: DependencyTag, exclude: DependencyTag) -> Vec<&Dependency>;
    fn runtime(&self) -> Vec<&Dependency>;
    fn build_time(&self) -> Vec<&Dependency>;
}

impl DependencyExt for Vec<Dependency> {
    fn filter_by_tags(&self, include: DependencyTag, exclude: DependencyTag) -> Vec<&Dependency> {
        self.iter()
            .filter(|dep| dep.tags.contains(include) && !dep.tags.intersects(exclude))
            .collect()
    }

    fn runtime(&self) -> Vec<&Dependency> {
        // A dependency is runtime if its tags indicate it's needed at runtime.
        // This includes standard runtime, recommended, or optional dependencies.
        // Build-only or Test-only dependencies (without other runtime flags) are excluded.
        self.iter()
            .filter(|dep| {
                dep.tags.intersects(
                    DependencyTag::RUNTIME | DependencyTag::RECOMMENDED | DependencyTag::OPTIONAL,
                )
            })
            .collect()
    }

    fn build_time(&self) -> Vec<&Dependency> {
        self.filter_by_tags(DependencyTag::BUILD, DependencyTag::empty())
    }
}

// src/model/mod.rs
// Declares the modules within the model directory.
use std::sync::Arc;

pub mod artifact;
pub mod cask;
pub mod formula;
pub mod tap;
pub mod version;

// Re-export
pub use artifact::InstalledArtifact;
pub use cask::Cask;
pub use formula::Formula;

#[derive(Debug, Clone)]
pub enum InstallTargetIdentifier {
    Formula(Arc<Formula>),
    Cask(Arc<Cask>),
}

// ===== sps-core/src/build/mod.rs =====
// Main module for build functionality
// Removed deprecated functions and re-exports.

use std::path::PathBuf;

use sps_common::config::Config;
use sps_common::model::formula::Formula;

// --- Submodules ---
pub mod bottle;
pub mod cask;
pub mod devtools;
pub mod extract;

// --- Path helpers using Config ---
pub fn get_formula_opt_path(formula: &Formula, config: &Config) -> PathBuf {
    // Use new Config method
    config.formula_opt_path(formula.name())
}

// --- DEPRECATED EXTRACTION FUNCTIONS REMOVED ---

pub mod app;
pub mod audio_unit_plugin;
pub mod binary;
pub mod colorpicker;
pub mod dictionary;
pub mod font;
pub mod input_method;
pub mod installer;
pub mod internet_plugin;
pub mod keyboard_layout;
pub mod manpage;
pub mod mdimporter;
pub mod pkg;
pub mod preflight;
pub mod prefpane;
pub mod qlplugin;
pub mod screen_saver;
pub mod service;
pub mod suite;
pub mod uninstall;
pub mod vst3_plugin;
pub mod vst_plugin;
pub mod zap;

// Re‑export a single enum if you like:
pub use self::app::install_app_from_staged;
pub use self::audio_unit_plugin::install_audio_unit_plugin;
pub use self::binary::install_binary;
pub use self::colorpicker::install_colorpicker;
pub use self::dictionary::install_dictionary;
pub use self::font::install_font;
pub use self::input_method::install_input_method;
pub use self::installer::run_installer;
pub use self::internet_plugin::install_internet_plugin;
pub use self::keyboard_layout::install_keyboard_layout;
pub use self::manpage::install_manpage;
pub use self::mdimporter::install_mdimporter;
pub use self::pkg::install_pkg_from_path;
pub use self::preflight::run_preflight;
pub use self::prefpane::install_prefpane;
pub use self::qlplugin::install_qlplugin;
pub use self::screen_saver::install_screen_saver;
pub use self::service::install_service;
pub use self::suite::install_suite;
pub use self::uninstall::record_uninstall;
pub use self::vst3_plugin::install_vst3_plugin;
pub use self::vst_plugin::install_vst_plugin;

// spm-fetch/src/lib.rs
pub mod api;
pub mod http;
pub mod oci;
pub mod validation;

// Re-export necessary types from sps-core IF using Option A from Step 3
// If using Option B (DTOs), you wouldn't depend on sps-core here for models.
// Re-export the public fetching functions - ensure they are `pub`
pub use api::{
    fetch_all_casks, fetch_all_formulas, fetch_cask, fetch_formula, get_cask, /* ... */
    get_formula,
};
pub use http::{fetch_formula_source_or_bottle, fetch_resource /* ... */};
pub use oci::{build_oci_client /* ... */, download_oci_blob, fetch_oci_manifest_index};
pub use sps_common::{
    model::{
        cask::{Sha256Field, UrlField},
        formula::ResourceSpec,
        Cask, Formula,
    }, // Example types needed
    {
        cache::Cache,
        error::{Result, SpsError},
        Config,
    }, // Need Config, Result, SpsError, Cache
};

pub use crate::validation::{validate_url, verify_checksum, verify_content_type /* ... */};

use std::sync::Arc;

use thiserror::Error;

#[derive(Error, Debug, Clone)]
pub enum SpsError {
    #[error("I/O Error: {0}")]
    Io(#[from] Arc<std::io::Error>),

    #[error("HTTP Request Error: {0}")]
    Http(#[from] Arc<reqwest::Error>),

    #[error("JSON Parsing Error: {0}")]
    Json(#[from] Arc<serde_json::Error>),

    #[error("Semantic Versioning Error: {0}")]
    SemVer(#[from] Arc<semver::Error>),

    #[error("Object File Error: {0}")]
    Object(#[from] Arc<object::read::Error>),

    #[error("Configuration Error: {0}")]
    Config(String),

    #[error("API Error: {0}")]
    Api(String),

    #[error("API Request Error: {0}")]
    ApiRequestError(String),

    #[error("DownloadError: Failed to download '{0}' from '{1}': {2}")]
    DownloadError(String, String, String),

    #[error("Cache Error: {0}")]
    Cache(String),

    #[error("Resource Not Found: {0}")]
    NotFound(String),

    #[error("Installation Error: {0}")]
    InstallError(String),

    #[error("Generic Error: {0}")]
    Generic(String),

    #[error("HttpError: {0}")]
    HttpError(String),

    #[error("Checksum Mismatch: {0}")]
    ChecksumMismatch(String),

    #[error("Validation Error: {0}")]
    ValidationError(String),

    #[error("Checksum Error: {0}")]
    ChecksumError(String),

    #[error("Parsing Error in {0}: {1}")]
    ParseError(&'static str, String),

    #[error("Version error: {0}")]
    VersionError(String),

    #[error("Dependency Error: {0}")]
    DependencyError(String),

    #[error("Build environment setup failed: {0}")]
    BuildEnvError(String),

    #[error("IoError: {0}")]
    IoError(String),

    #[error("Failed to execute command: {0}")]
    CommandExecError(String),

    #[error("Mach-O Error: {0}")]
    MachOError(String),

    #[error("Mach-O Modification Error: {0}")]
    MachOModificationError(String),

    #[error("Mach-O Relocation Error: Path too long - {0}")]
    PathTooLongError(String),

    #[error("Codesign Error: {0}")]
    CodesignError(String),
}

impl From<std::io::Error> for SpsError {
    fn from(err: std::io::Error) -> Self {
        SpsError::Io(Arc::new(err))
    }
}

impl From<reqwest::Error> for SpsError {
    fn from(err: reqwest::Error) -> Self {
        SpsError::Http(Arc::new(err))
    }
}

impl From<serde_json::Error> for SpsError {
    fn from(err: serde_json::Error) -> Self {
        SpsError::Json(Arc::new(err))
    }
}

impl From<semver::Error> for SpsError {
    fn from(err: semver::Error) -> Self {
        SpsError::SemVer(Arc::new(err))
    }
}

impl From<object::read::Error> for SpsError {
    fn from(err: object::read::Error) -> Self {
        SpsError::Object(Arc::new(err))
    }
}

pub type Result<T> = std::result::Result<T, SpsError>;

// sps-common/src/lib.rs
pub mod cache;
pub mod config;
pub mod dependency;
pub mod error;
pub mod formulary;
pub mod keg;
pub mod model;
pub mod pipeline;
// Optional: pub mod dependency_def;

// Re-export key types
pub use cache::Cache;
pub use config::Config;
pub use error::{Result, SpsError};
pub use model::{Cask, Formula, InstalledArtifact}; // etc.
                                                   // Optional: pub use dependency_def::{Dependency, DependencyTag};

// **File:** sps-core/src/dependency/requirement.rs (New file)
use std::fmt;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Requirement {
    MacOS(String),
    Xcode(String),
    Other(String),
}

impl fmt::Display for Requirement {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MacOS(v) => write!(f, "macOS >= {v}"),
            Self::Xcode(v) => write!(f, "Xcode >= {v}"),
            Self::Other(s) => write!(f, "Requirement: {s}"),
        }
    }
}

// sps-core/src/uninstall/mod.rs

pub mod cask;
pub mod common;
pub mod formula;

// Re-export key functions and types
pub use cask::{uninstall_cask_artifacts, zap_cask_artifacts};
pub use common::UninstallOptions;
pub use formula::uninstall_formula_artifacts;

// sps-core/src/lib.rs

// Declare the top-level modules within the library crate
pub mod build;
pub mod check;
pub mod install;
pub mod pipeline;
pub mod uninstall;
pub mod upgrade; // New
#[cfg(target_os = "macos")]
pub mod utils; // New
               //pub mod utils;

// Re-export key types for easier use by the CLI crate
// Define InstallTargetIdentifier here or ensure it's public from cli/pipeline
// For simplicity, let's define it here for now:

// New
pub use uninstall::UninstallOptions; // New
                                     // New

// sps-core/src/upgrade/mod.rs

pub mod bottle;
pub mod cask;
pub mod source;

// Re-export key upgrade functions
pub use self::bottle::upgrade_bottle_formula;
pub use self::cask::upgrade_cask_package;
pub use self::source::upgrade_source_formula;

pub mod installed;
pub mod update;

pub use installed::{InstalledPackageInfo, PackageType};
pub use update::UpdateInfo;

pub mod definition; // Renamed from 'dependency'
pub mod requirement;
pub mod resolver;

// Re-export key types for easier access
pub use definition::{Dependency, DependencyExt, DependencyTag}; // Updated source module
pub use requirement::Requirement;
pub use resolver::{
    DependencyResolver, ResolutionContext, ResolutionStatus, ResolvedDependency, ResolvedGraph,
};

#[cfg(target_os = "macos")]
pub mod applescript;
#[cfg(target_os = "macos")]
pub mod xattr;

pub mod downloader;
pub mod planner;
pub mod runner;

pub fn install_suite (
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {}

This code segment functions to: Transfer application bundle directories from a temporary staging area to a system-wide applications location while maintaining version-specific references through symbolic links. It handles existing installations by performing cleanup operations, ensures successful file movement with fallback copying, and tracks installed components for management purposes.

```rust
[TASK_BEGIN]
pub fn install_suite(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut artifacts = Vec::new();

    for suite_name in &cask.artifacts.suite {
        debug!("Installing suite artifact: {}", suite_name);

        let source_path = stage_path.join(suite_name);
        let dest_path = config.applications_dir.join(suite_name);

        // Remove existing application directory if present
        remove_path_robustly(&dest_path)?;

        // Move from staging to final Applications location
        std::fs::rename(&source_path, &dest_path)?;

        // Create symlink in Caskroom directory
        let symlink_path = cask_version_install_path.join(suite_name);
        symlink(&dest_path, &symlink_path)?;

        artifacts.push(InstalledArtifact {
            path: symlink_path,
            // Additional fields would be populated here based on artifact metadata
        });
    }

    Ok(artifacts)
}
[TASK_END]
```

pub fn install_suite(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut installed = Vec::new();
    if let Some(artifacts_def) = &cask.artifacts {
        for art in artifacts_def.iter() {
            if let Some(obj) = art.as_object() {
                if let Some(entries) = obj.get("suite").and_then(|v| v.as_array()) {
                    for entry in entries {
                        if let Some(dir_name) = entry.as_str() {
                            let src = stage_path.join(dir_name);
                            if !src.exists() {
                                debug!(
                                    "Suite directory '{}' not found in staging, skipping",
                                    dir_name
                                );
                                continue;
                            }
                            let dest_dir = config.applications_dir();
                            let dest = dest_dir.join(dir_name);
                            if dest.exists() {
                                let _ = remove_path_robustly(&dest, config, true);
                            }
                            debug!("Moving suite '{}' → '{}'", src.display(), dest.display());
                            let mv_status = Command::new("mv").arg(&src).arg(&dest).status()?;
                            if !mv_status.success() {
                                Command::new("cp").arg("-R").arg(&src).arg(&dest).status()?;
                            }
                            installed.push(InstalledArtifact::AppBundle { path: dest.clone() });
                            let link = cask_version_install_path.join(dir_name);
                            let _ = remove_path_robustly(&link, config, true);
                            symlink(&dest, &link)?;
                            installed.push(InstalledArtifact::CaskroomLink {
                                link_path: link,
                                target_path: dest.clone(),
                            });
                        }
                    }
                    break;
                }
            }
        }
    }
    Ok(installed)
}

pub fn install_suite(
    cask: &Cask,
    stage_path: &Path,
    cask_version_install_path: &Path,
    config: &Config,
) -> Result<Vec<InstalledArtifact>> {
    let mut artifacts = Vec::new();
    for suite_name in &cask.artifacts.suite {
        debug!("Installing suite artifact: {}", suite_name);
        let source_path = stage_path.join(suite_name);
        let dest_path = config.applications_dir.join(suite_name);
        remove_path_robustly(&dest_path)?;
        std::fs::rename(&source_path, &dest_path)?;
        let symlink_path = cask_version_install_path.join(suite_name);
        symlink(&dest_path, &symlink_path)?;
        artifacts.push(InstalledArtifact {
            path: symlink_path,
        });
    }
    Ok(artifacts)
}