arch_toolkit/deps/
resolve.rs

1//! Core dependency resolution logic for individual packages.
2//!
3//! This module provides functions to resolve dependencies for packages, determine
4//! dependency status, and handle batch operations for efficient dependency resolution.
5
6use crate::deps::parse::{parse_dep_spec, parse_pacman_si_conflicts, parse_pacman_si_deps};
7use crate::deps::pkgbuild::parse_pkgbuild_deps;
8use crate::deps::query::{
9    get_available_version, get_installed_packages, get_installed_version, get_provided_packages,
10    get_upgradable_packages, is_package_installed_or_provided,
11};
12use crate::deps::source::{determine_dependency_source, is_system_package};
13use crate::deps::version::version_satisfies;
14use crate::error::Result;
15use crate::types::dependency::{
16    Dependency, DependencySource, DependencyStatus, PackageRef, PackageSource, ResolverConfig,
17};
18use std::collections::{HashMap, HashSet};
19use std::hash::BuildHasher;
20use std::process::{Command, Stdio};
21
22/// Type alias for PKGBUILD cache callback function.
23type PkgbuildCacheFn = dyn Fn(&str) -> Option<String> + Send + Sync;
24
25/// What: Evaluate a dependency's installation status relative to required versions.
26///
27/// Inputs:
28/// - `name`: Dependency package identifier.
29/// - `version_req`: Optional version constraint string (e.g., `>=1.2`).
30/// - `installed`: Set of names currently installed on the system.
31/// - `provided`: Set of package names provided by installed packages.
32/// - `upgradable`: Set of names pacman reports as upgradable.
33///
34/// Output:
35/// - Returns a `DependencyStatus` describing whether installation, upgrade, or no action is needed.
36///
37/// Details:
38/// - Combines local database queries with helper functions to capture upgrade requirements.
39/// - Uses `is_package_installed_or_provided()` to check if package is available.
40/// - Uses `get_installed_version()` and `get_available_version()` for version checking.
41/// - Uses `version_satisfies()` for version requirement validation.
42///
43/// # Example
44///
45/// ```no_run
46/// use arch_toolkit::deps::determine_status;
47/// use std::collections::HashSet;
48///
49/// let installed = HashSet::from(["glibc".to_string()]);
50/// let provided = HashSet::new();
51/// let upgradable = HashSet::new();
52///
53/// let status = determine_status("glibc", "", &installed, &provided, &upgradable);
54/// println!("Status: {:?}", status);
55/// ```
56pub fn determine_status<S: BuildHasher>(
57    name: &str,
58    version_req: &str,
59    installed: &HashSet<String, S>,
60    provided: &HashSet<String, S>,
61    upgradable: &HashSet<String, S>,
62) -> DependencyStatus {
63    // Check if package is installed or provided by an installed package
64    if !is_package_installed_or_provided(name, installed, provided) {
65        return DependencyStatus::ToInstall;
66    }
67
68    // Check if package is upgradable (even without version requirement)
69    let is_upgradable = upgradable.contains(name);
70
71    // If version requirement is specified, check if it matches
72    if !version_req.is_empty() {
73        // Try to get installed version
74        if let Ok(installed_version) = get_installed_version(name) {
75            // Check if version requirement is satisfied
76            if !version_satisfies(&installed_version, version_req) {
77                return DependencyStatus::ToUpgrade {
78                    current: installed_version,
79                    required: version_req.to_string(),
80                };
81            }
82            // Version requirement satisfied, but check if package is upgradable anyway
83            if is_upgradable {
84                // Get available version from pacman -Si if possible
85                let available_version =
86                    get_available_version(name).unwrap_or_else(|| "newer".to_string());
87                return DependencyStatus::ToUpgrade {
88                    current: installed_version,
89                    required: available_version,
90                };
91            }
92            return DependencyStatus::Installed {
93                version: installed_version,
94            };
95        }
96    }
97
98    // Installed but no version check needed - check if upgradable
99    if is_upgradable {
100        match get_installed_version(name) {
101            Ok(current_version) => {
102                let available_version =
103                    get_available_version(name).unwrap_or_else(|| "newer".to_string());
104                return DependencyStatus::ToUpgrade {
105                    current: current_version,
106                    required: available_version,
107                };
108            }
109            Err(_) => {
110                return DependencyStatus::ToUpgrade {
111                    current: "installed".to_string(),
112                    required: "newer".to_string(),
113                };
114            }
115        }
116    }
117
118    // Installed and up-to-date - get actual version
119    get_installed_version(name).map_or_else(
120        |_| DependencyStatus::Installed {
121            version: "installed".to_string(),
122        },
123        |version| DependencyStatus::Installed { version },
124    )
125}
126
127/// What: Batch fetch dependency lists for multiple official packages using `pacman -Si`.
128///
129/// Inputs:
130/// - `names`: Package names to query (must be official packages, not local).
131///
132/// Output:
133/// - `HashMap` mapping package name to its dependency list (`Vec<String>`).
134///
135/// Details:
136/// - Batches queries into chunks of 50 to avoid command-line length limits.
137/// - Parses multi-package `pacman -Si` output (packages separated by blank lines).
138/// - Gracefully handles command failures by returning partial results.
139///
140/// # Example
141///
142/// ```no_run
143/// use arch_toolkit::deps::batch_fetch_official_deps;
144///
145/// let packages = vec!["firefox", "vim"];
146/// let deps = batch_fetch_official_deps(&packages);
147/// println!("Found dependencies for {} packages", deps.len());
148/// ```
149#[must_use]
150pub fn batch_fetch_official_deps(names: &[&str]) -> HashMap<String, Vec<String>> {
151    const BATCH_SIZE: usize = 50;
152    let mut result_map = HashMap::new();
153
154    for chunk in names.chunks(BATCH_SIZE) {
155        let mut args = vec!["-Si"];
156        args.extend(chunk.iter().copied());
157        match Command::new("pacman")
158            .args(&args)
159            .env("LC_ALL", "C")
160            .env("LANG", "C")
161            .stdin(Stdio::null())
162            .stdout(Stdio::piped())
163            .stderr(Stdio::piped())
164            .output()
165        {
166            Ok(output) if output.status.success() => {
167                let text = String::from_utf8_lossy(&output.stdout);
168                // Parse multi-package output: packages are separated by blank lines
169                let mut package_blocks = Vec::new();
170                let mut current_block = String::new();
171                for line in text.lines() {
172                    if line.trim().is_empty() {
173                        if !current_block.is_empty() {
174                            package_blocks.push(current_block.clone());
175                            current_block.clear();
176                        }
177                    } else {
178                        current_block.push_str(line);
179                        current_block.push('\n');
180                    }
181                }
182                if !current_block.is_empty() {
183                    package_blocks.push(current_block);
184                }
185
186                // Parse each block to extract package name and dependencies
187                for block in package_blocks {
188                    let dep_names = parse_pacman_si_deps(&block);
189                    // Extract package name from block
190                    if let Some(name_line) =
191                        block.lines().find(|l| l.trim_start().starts_with("Name"))
192                        && let Some((_, name)) = name_line.split_once(':')
193                    {
194                        let pkg_name = name.trim().to_string();
195                        result_map.insert(pkg_name, dep_names);
196                    }
197                }
198            }
199            _ => {
200                // If batch fails, fall back to individual queries (but don't do it here to avoid recursion)
201                // The caller will handle individual queries
202                break;
203            }
204        }
205    }
206    result_map
207}
208
209/// What: Check if a command is available in PATH.
210///
211/// Inputs:
212/// - `cmd`: Command name to check.
213///
214/// Output:
215/// - Returns true if the command exists and can be executed.
216///
217/// Details:
218/// - Uses a simple version check to verify command availability.
219fn is_command_available(cmd: &str) -> bool {
220    Command::new(cmd)
221        .args(["--version"])
222        .stdin(Stdio::null())
223        .stdout(Stdio::null())
224        .stderr(Stdio::null())
225        .output()
226        .is_ok()
227}
228
229/// What: Check if a package name should be filtered out (virtual package or self-reference).
230///
231/// Inputs:
232/// - `pkg_name`: Package name to check.
233/// - `parent_name`: Name of the parent package (to detect self-references).
234///
235/// Output:
236/// - Returns true if the package should be filtered out.
237///
238/// Details:
239/// - Filters out .so files (virtual packages) and self-references.
240#[allow(clippy::case_sensitive_file_extension_comparisons)]
241fn should_filter_dependency(pkg_name: &str, parent_name: &str) -> bool {
242    let pkg_lower = pkg_name.to_lowercase();
243    pkg_name == parent_name
244        || pkg_lower.ends_with(".so")
245        || pkg_lower.contains(".so.")
246        || pkg_lower.contains(".so=")
247}
248
249/// What: Convert a dependency spec into a `Dependency` record.
250///
251/// Inputs:
252/// - `dep_spec`: Dependency specification string (may include version requirements).
253/// - `parent_name`: Name of the package that requires this dependency.
254/// - `installed`: Set of locally installed packages.
255/// - `provided`: Set of package names provided by installed packages.
256/// - `upgradable`: Set of packages flagged for upgrades.
257///
258/// Output:
259/// - Returns Some(Dependency) if the dependency should be included, None if filtered.
260///
261/// Details:
262/// - Parses the dependency spec, filters out virtual packages and self-references,
263///   and determines status, source, and system package flags.
264fn process_dependency_spec<S: BuildHasher>(
265    dep_spec: &str,
266    parent_name: &str,
267    installed: &HashSet<String, S>,
268    provided: &HashSet<String, S>,
269    upgradable: &HashSet<String, S>,
270) -> Option<Dependency> {
271    let spec = parse_dep_spec(dep_spec);
272    let pkg_name = spec.name;
273    let version_req = spec.version_req;
274
275    if should_filter_dependency(&pkg_name, parent_name) {
276        if pkg_name == parent_name {
277            tracing::debug!("Skipping self-reference: {} == {}", pkg_name, parent_name);
278        } else {
279            tracing::debug!("Filtering out virtual package: {}", pkg_name);
280        }
281        return None;
282    }
283
284    let status = determine_status(&pkg_name, &version_req, installed, provided, upgradable);
285    let (source, is_core) = determine_dependency_source(&pkg_name, installed);
286    let is_system = is_core || is_system_package(&pkg_name);
287
288    Some(Dependency {
289        name: pkg_name,
290        version_req,
291        status,
292        source,
293        required_by: vec![parent_name.to_string()],
294        depends_on: Vec::new(),
295        is_core,
296        is_system,
297    })
298}
299
300/// What: Process a list of dependency specs into `Dependency` records.
301///
302/// Inputs:
303/// - `dep_specs`: Vector of dependency specification strings.
304/// - `parent_name`: Name of the package that requires these dependencies.
305/// - `installed`: Set of locally installed packages.
306/// - `provided`: Set of package names provided by installed packages.
307/// - `upgradable`: Set of packages flagged for upgrades.
308///
309/// Output:
310/// - Returns a vector of `Dependency` records (filtered).
311///
312/// Details:
313/// - Processes each dependency spec and collects valid dependencies.
314fn process_dependency_specs<S: BuildHasher>(
315    dep_specs: Vec<String>,
316    parent_name: &str,
317    installed: &HashSet<String, S>,
318    provided: &HashSet<String, S>,
319    upgradable: &HashSet<String, S>,
320) -> Vec<Dependency> {
321    dep_specs
322        .into_iter()
323        .filter_map(|dep_spec| {
324            process_dependency_spec(&dep_spec, parent_name, installed, provided, upgradable)
325        })
326        .collect()
327}
328
329/// What: Resolve dependencies for a local package using pacman -Qi.
330///
331/// Inputs:
332/// - `name`: Package name.
333/// - `installed`: Set of locally installed packages.
334/// - `provided`: Set of package names provided by installed packages.
335/// - `upgradable`: Set of packages flagged for upgrades.
336///
337/// Output:
338/// - Returns a vector of `Dependency` records or an error string.
339///
340/// Details:
341/// - Uses pacman -Qi to get dependency information for locally installed packages.
342fn resolve_local_package_deps<S: BuildHasher>(
343    name: &str,
344    installed: &HashSet<String, S>,
345    provided: &HashSet<String, S>,
346    upgradable: &HashSet<String, S>,
347) -> Result<Vec<Dependency>> {
348    tracing::debug!("Running: pacman -Qi {} (local package)", name);
349    let output = Command::new("pacman")
350        .args(["-Qi", name])
351        .env("LC_ALL", "C")
352        .env("LANG", "C")
353        .stdin(Stdio::null())
354        .stdout(Stdio::piped())
355        .stderr(Stdio::piped())
356        .output()
357        .map_err(|e| {
358            tracing::error!("Failed to execute pacman -Qi {}: {}", name, e);
359            crate::error::ArchToolkitError::Parse(format!("pacman -Qi failed: {e}"))
360        })?;
361
362    if !output.status.success() {
363        let stderr = String::from_utf8_lossy(&output.stderr);
364        tracing::warn!(
365            "pacman -Qi {} failed with status {:?}: {}",
366            name,
367            output.status.code(),
368            stderr
369        );
370        return Ok(Vec::new());
371    }
372
373    let text = String::from_utf8_lossy(&output.stdout);
374    tracing::debug!("pacman -Qi {} output ({} bytes)", name, text.len());
375
376    let dep_names = parse_pacman_si_deps(&text);
377    tracing::debug!(
378        "Parsed {} dependency names from pacman -Qi output",
379        dep_names.len()
380    );
381
382    Ok(process_dependency_specs(
383        dep_names, name, installed, provided, upgradable,
384    ))
385}
386
387/// What: Resolve dependencies for an official package using pacman -Si.
388///
389/// Inputs:
390/// - `name`: Package name.
391/// - `repo`: Repository name (for logging).
392/// - `installed`: Set of locally installed packages.
393/// - `provided`: Set of package names provided by installed packages.
394/// - `upgradable`: Set of packages flagged for upgrades.
395///
396/// Output:
397/// - Returns a vector of `Dependency` records or an error string.
398///
399/// Details:
400/// - Uses pacman -Si to get dependency information for official packages.
401fn resolve_official_package_deps<S: BuildHasher>(
402    name: &str,
403    repo: &str,
404    installed: &HashSet<String, S>,
405    provided: &HashSet<String, S>,
406    upgradable: &HashSet<String, S>,
407) -> Result<Vec<Dependency>> {
408    tracing::debug!("Running: pacman -Si {} (repo: {})", name, repo);
409    let output = Command::new("pacman")
410        .args(["-Si", name])
411        .env("LC_ALL", "C")
412        .env("LANG", "C")
413        .stdin(Stdio::null())
414        .stdout(Stdio::piped())
415        .stderr(Stdio::piped())
416        .output()
417        .map_err(|e| {
418            tracing::error!("Failed to execute pacman -Si {}: {}", name, e);
419            crate::error::ArchToolkitError::Parse(format!("pacman -Si failed: {e}"))
420        })?;
421
422    if !output.status.success() {
423        let stderr = String::from_utf8_lossy(&output.stderr);
424        tracing::error!(
425            "pacman -Si {} failed with status {:?}: {}",
426            name,
427            output.status.code(),
428            stderr
429        );
430        return Err(crate::error::ArchToolkitError::Parse(format!(
431            "pacman -Si failed for {name}: {stderr}"
432        )));
433    }
434
435    let text = String::from_utf8_lossy(&output.stdout);
436    tracing::debug!("pacman -Si {} output ({} bytes)", name, text.len());
437
438    let dep_names = parse_pacman_si_deps(&text);
439    tracing::debug!(
440        "Parsed {} dependency names from pacman -Si output",
441        dep_names.len()
442    );
443
444    Ok(process_dependency_specs(
445        dep_names, name, installed, provided, upgradable,
446    ))
447}
448
449/// What: Try to resolve dependencies using an AUR helper (paru or yay).
450///
451/// Inputs:
452/// - `helper`: Helper command name ("paru" or "yay").
453/// - `name`: Package name.
454/// - `installed`: Set of locally installed packages.
455/// - `provided`: Set of package names provided by installed packages.
456/// - `upgradable`: Set of packages flagged for upgrades.
457///
458/// Output:
459/// - Returns Some(Vec<Dependency>) if successful, None otherwise.
460///
461/// Details:
462/// - Executes helper -Si command and parses the output for dependencies.
463fn try_helper_resolution<S: BuildHasher>(
464    helper: &str,
465    name: &str,
466    installed: &HashSet<String, S>,
467    provided: &HashSet<String, S>,
468    upgradable: &HashSet<String, S>,
469) -> Option<Vec<Dependency>> {
470    tracing::debug!("Trying {} -Si {} for dependency resolution", helper, name);
471    let output = Command::new(helper)
472        .args(["-Si", name])
473        .env("LC_ALL", "C")
474        .env("LANG", "C")
475        .stdin(Stdio::null())
476        .stdout(Stdio::piped())
477        .stderr(Stdio::piped())
478        .output()
479        .ok()?;
480
481    if !output.status.success() {
482        let stderr = String::from_utf8_lossy(&output.stderr);
483        tracing::debug!(
484            "{} -Si {} failed (will try other methods): {}",
485            helper,
486            name,
487            stderr.trim()
488        );
489        return None;
490    }
491
492    let text = String::from_utf8_lossy(&output.stdout);
493    tracing::debug!("{} -Si {} output ({} bytes)", helper, name, text.len());
494    let dep_names = parse_pacman_si_deps(&text);
495
496    if dep_names.is_empty() {
497        return None;
498    }
499
500    tracing::info!(
501        "Using {} to resolve runtime dependencies for {} (will fetch .SRCINFO for build-time deps)",
502        helper,
503        name
504    );
505
506    let deps = process_dependency_specs(dep_names, name, installed, provided, upgradable);
507    Some(deps)
508}
509
510/// What: Enhance dependency list with .SRCINFO data.
511///
512/// Inputs:
513/// - `name`: Package name.
514/// - `deps`: Existing dependency list to enhance.
515/// - `installed`: Set of locally installed packages.
516/// - `provided`: Set of package names provided by installed packages.
517/// - `upgradable`: Set of packages flagged for upgrades.
518///
519/// Output:
520/// - Returns the enhanced dependency list.
521///
522/// Details:
523/// - Fetches and parses .SRCINFO to add missing depends entries.
524/// - Requires `feature = "aur"` to be enabled.
525#[cfg(feature = "aur")]
526fn enhance_with_srcinfo<S: BuildHasher>(
527    name: &str,
528    deps: Vec<Dependency>,
529    _installed: &HashSet<String, S>,
530    _provided: &HashSet<String, S>,
531    _upgradable: &HashSet<String, S>,
532) -> Vec<Dependency> {
533    // Note: fetch_srcinfo is async and requires a reqwest client, so we can't use it here
534    // in the sync context. This is a limitation - callers should fetch .SRCINFO separately
535    // if needed, or use the async API when available.
536    tracing::debug!(
537        "Skipping .SRCINFO enhancement for {} (requires async context)",
538        name
539    );
540    deps
541}
542
543/// What: Enhance dependency list with .SRCINFO data (no-op when AUR feature is disabled).
544///
545/// Inputs:
546/// - `name`: Package name (unused).
547/// - `deps`: Existing dependency list to return as-is.
548/// - `installed`: Set of locally installed packages (unused).
549/// - `provided`: Set of package names provided by installed packages (unused).
550/// - `upgradable`: Set of packages flagged for upgrades (unused).
551///
552/// Output:
553/// - Returns the dependency list unchanged.
554///
555/// Details:
556/// - This is a no-op when `feature = "aur"` is not enabled.
557#[cfg(not(feature = "aur"))]
558#[allow(clippy::unused_parameters)]
559fn enhance_with_srcinfo<S: BuildHasher>(
560    _name: &str,
561    deps: Vec<Dependency>,
562    _installed: &HashSet<String, S>,
563    _provided: &HashSet<String, S>,
564    _upgradable: &HashSet<String, S>,
565) -> Vec<Dependency> {
566    deps
567}
568
569/// What: Fallback to cached PKGBUILD for dependency resolution.
570///
571/// Inputs:
572/// - `name`: Package name.
573/// - `pkgbuild_cache`: Optional callback to fetch PKGBUILD from cache.
574/// - `installed`: Set of locally installed packages.
575/// - `provided`: Set of package names provided by installed packages.
576/// - `upgradable`: Set of packages flagged for upgrades.
577///
578/// Output:
579/// - Returns a vector of `Dependency` records if `PKGBUILD` is found, empty vector otherwise.
580///
581/// Details:
582/// - Attempts to use cached PKGBUILD when .SRCINFO is unavailable (offline fallback).
583fn fallback_to_pkgbuild<S: BuildHasher>(
584    name: &str,
585    pkgbuild_cache: Option<&PkgbuildCacheFn>,
586    installed: &HashSet<String, S>,
587    provided: &HashSet<String, S>,
588    upgradable: &HashSet<String, S>,
589) -> Vec<Dependency> {
590    let Some(pkgbuild_text) = pkgbuild_cache.and_then(|f| f(name)) else {
591        tracing::debug!(
592            "No cached PKGBUILD available for {} (offline, no dependencies resolved)",
593            name
594        );
595        return Vec::new();
596    };
597
598    tracing::info!(
599        "Using cached PKGBUILD for {} to resolve dependencies (offline fallback)",
600        name
601    );
602    let (pkgbuild_depends, _, _, _) = parse_pkgbuild_deps(&pkgbuild_text);
603
604    let deps = process_dependency_specs(pkgbuild_depends, name, installed, provided, upgradable);
605    tracing::info!(
606        "Resolved {} dependencies from cached PKGBUILD for {}",
607        deps.len(),
608        name
609    );
610    deps
611}
612
613/// What: Resolve dependencies for an AUR package.
614///
615/// Inputs:
616/// - `name`: Package name.
617/// - `installed`: Set of locally installed packages.
618/// - `provided`: Set of package names provided by installed packages.
619/// - `upgradable`: Set of packages flagged for upgrades.
620/// - `pkgbuild_cache`: Optional callback to fetch PKGBUILD from cache.
621///
622/// Output:
623/// - Returns a vector of `Dependency` records.
624///
625/// Details:
626/// - Tries paru/yay first, then falls back to .SRCINFO and cached PKGBUILD.
627fn resolve_aur_package_deps<S: BuildHasher>(
628    name: &str,
629    installed: &HashSet<String, S>,
630    provided: &HashSet<String, S>,
631    upgradable: &HashSet<String, S>,
632    pkgbuild_cache: Option<&PkgbuildCacheFn>,
633) -> Vec<Dependency> {
634    tracing::debug!(
635        "Attempting to resolve AUR package: {} (will skip if not found)",
636        name
637    );
638
639    // Try paru first
640    let (mut deps, mut used_helper) = if is_command_available("paru")
641        && let Some(helper_deps) =
642            try_helper_resolution("paru", name, installed, provided, upgradable)
643    {
644        (helper_deps, true)
645    } else {
646        (Vec::new(), false)
647    };
648
649    // Try yay if paru didn't work
650    if !used_helper
651        && is_command_available("yay")
652        && let Some(helper_deps) =
653            try_helper_resolution("yay", name, installed, provided, upgradable)
654    {
655        deps = helper_deps;
656        used_helper = true;
657    }
658
659    if !used_helper {
660        tracing::debug!(
661            "Skipping AUR API for {} - paru/yay failed or not available (likely not a real package)",
662            name
663        );
664    }
665
666    // Always try to enhance with .SRCINFO
667    deps = enhance_with_srcinfo(name, deps, installed, provided, upgradable);
668
669    // Fallback to PKGBUILD if no dependencies were found
670    if !used_helper && deps.is_empty() {
671        deps = fallback_to_pkgbuild(name, pkgbuild_cache, installed, provided, upgradable);
672    }
673
674    deps
675}
676
677/// What: Resolve direct dependency metadata for a single package.
678///
679/// Inputs:
680/// - `name`: Package identifier whose dependencies should be enumerated.
681/// - `source`: Source enum describing whether the package is official or AUR.
682/// - `installed`: Set of locally installed packages for status determination.
683/// - `provided`: Set of package names provided by installed packages.
684/// - `upgradable`: Set of packages flagged for upgrades, used to detect stale dependencies.
685/// - `pkgbuild_cache`: Optional callback to fetch PKGBUILD from cache.
686///
687/// Output:
688/// - Returns a vector of `Dependency` records or an error string when resolution fails.
689///
690/// Details:
691/// - Invokes pacman or AUR helpers depending on source, filtering out virtual entries and self references.
692fn resolve_package_deps<S: BuildHasher>(
693    name: &str,
694    source: &PackageSource,
695    installed: &HashSet<String, S>,
696    provided: &HashSet<String, S>,
697    upgradable: &HashSet<String, S>,
698    pkgbuild_cache: Option<&PkgbuildCacheFn>,
699) -> Result<Vec<Dependency>> {
700    let deps = match source {
701        PackageSource::Official { repo, .. } => {
702            if repo == "local" {
703                resolve_local_package_deps(name, installed, provided, upgradable)?
704            } else {
705                resolve_official_package_deps(name, repo, installed, provided, upgradable)?
706            }
707        }
708        PackageSource::Aur => {
709            resolve_aur_package_deps(name, installed, provided, upgradable, pkgbuild_cache)
710        }
711    };
712
713    tracing::debug!("Resolved {} dependencies for package {}", deps.len(), name);
714    Ok(deps)
715}
716
717/// What: Fetch conflicts for a package from pacman or AUR sources.
718///
719/// Inputs:
720/// - `name`: Package identifier.
721/// - `source`: Source enum describing whether the package is official or AUR.
722///
723/// Output:
724/// - Returns a vector of conflicting package names, or empty vector on error.
725///
726/// Details:
727/// - For official packages, uses `pacman -Si` to get conflicts.
728/// - For AUR packages, tries paru/yay first, then falls back to .SRCINFO.
729///
730/// # Example
731///
732/// ```no_run
733/// use arch_toolkit::deps::fetch_package_conflicts;
734/// use arch_toolkit::PackageSource;
735///
736/// let conflicts = fetch_package_conflicts(
737///     "firefox",
738///     &PackageSource::Official {
739///         repo: "extra".into(),
740///         arch: "x86_64".into(),
741///     },
742/// );
743/// println!("Found {} conflicts", conflicts.len());
744/// ```
745pub fn fetch_package_conflicts(name: &str, source: &PackageSource) -> Vec<String> {
746    match source {
747        PackageSource::Official { repo, .. } => {
748            // Handle local packages specially - use pacman -Qi instead of -Si
749            if repo == "local" {
750                tracing::debug!("Running: pacman -Qi {} (local package, conflicts)", name);
751                if let Ok(output) = Command::new("pacman")
752                    .args(["-Qi", name])
753                    .env("LC_ALL", "C")
754                    .env("LANG", "C")
755                    .stdin(Stdio::null())
756                    .stdout(Stdio::piped())
757                    .stderr(Stdio::piped())
758                    .output()
759                    && output.status.success()
760                {
761                    let text = String::from_utf8_lossy(&output.stdout);
762                    return parse_pacman_si_conflicts(&text);
763                }
764                return Vec::new();
765            }
766
767            // Use pacman -Si to get conflicts
768            tracing::debug!("Running: pacman -Si {} (conflicts)", name);
769            if let Ok(output) = Command::new("pacman")
770                .args(["-Si", name])
771                .env("LC_ALL", "C")
772                .env("LANG", "C")
773                .stdin(Stdio::null())
774                .stdout(Stdio::piped())
775                .stderr(Stdio::piped())
776                .output()
777                && output.status.success()
778            {
779                let text = String::from_utf8_lossy(&output.stdout);
780                return parse_pacman_si_conflicts(&text);
781            }
782            Vec::new()
783        }
784        PackageSource::Aur => {
785            // Try paru/yay first
786            let has_paru = is_command_available("paru");
787            let has_yay = is_command_available("yay");
788
789            if has_paru {
790                tracing::debug!("Trying paru -Si {} for conflicts", name);
791                if let Ok(output) = Command::new("paru")
792                    .args(["-Si", name])
793                    .env("LC_ALL", "C")
794                    .env("LANG", "C")
795                    .stdin(Stdio::null())
796                    .stdout(Stdio::piped())
797                    .stderr(Stdio::piped())
798                    .output()
799                    && output.status.success()
800                {
801                    let text = String::from_utf8_lossy(&output.stdout);
802                    let conflicts = parse_pacman_si_conflicts(&text);
803                    if !conflicts.is_empty() {
804                        return conflicts;
805                    }
806                }
807            }
808
809            if has_yay {
810                tracing::debug!("Trying yay -Si {} for conflicts", name);
811                if let Ok(output) = Command::new("yay")
812                    .args(["-Si", name])
813                    .env("LC_ALL", "C")
814                    .env("LANG", "C")
815                    .stdin(Stdio::null())
816                    .stdout(Stdio::piped())
817                    .stderr(Stdio::piped())
818                    .output()
819                    && output.status.success()
820                {
821                    let text = String::from_utf8_lossy(&output.stdout);
822                    let conflicts = parse_pacman_si_conflicts(&text);
823                    if !conflicts.is_empty() {
824                        return conflicts;
825                    }
826                }
827            }
828
829            // Fall back to .SRCINFO
830            // Note: fetch_srcinfo is async, so we can't use it here in sync context
831            // This is a limitation - conflicts from .SRCINFO won't be detected in sync mode
832            #[cfg(feature = "aur")]
833            {
834                tracing::debug!(
835                    "Skipping .SRCINFO conflict check for {} (requires async context)",
836                    name
837                );
838            }
839
840            Vec::new()
841        }
842    }
843}
844
845/// What: Get priority value for dependency status (lower = more urgent).
846///
847/// Inputs:
848/// - `status`: Dependency status to get priority for.
849///
850/// Output:
851/// - Returns a numeric priority where lower numbers indicate higher urgency.
852///
853/// Details:
854/// - Priority order: Conflict (0) < Missing (1) < `ToInstall` (2) < `ToUpgrade` (3) < Installed (4).
855const fn dependency_priority(status: &DependencyStatus) -> u8 {
856    status.priority()
857}
858
859/// What: Merge a dependency into the dependency map.
860///
861/// Inputs:
862/// - `dep`: Dependency to merge.
863/// - `parent_name`: Name of the package that requires this dependency.
864/// - `installed`: Set of installed package names.
865/// - `provided`: Set of provided packages.
866/// - `upgradable`: Set of upgradable package names.
867/// - `deps`: Mutable reference to the dependency map to update.
868///
869/// Output:
870/// - Updates the `deps` map with the merged dependency.
871///
872/// Details:
873/// - Merges status (keeps worst), version requirements (keeps more restrictive), and `required_by` lists.
874fn merge_dependency<S: BuildHasher>(
875    dep: &Dependency,
876    parent_name: &str,
877    installed: &HashSet<String, S>,
878    provided: &HashSet<String, S>,
879    upgradable: &HashSet<String, S>,
880    deps: &mut HashMap<String, Dependency>,
881) {
882    let dep_name = dep.name.clone();
883
884    // Check if dependency already exists and get its current state
885    let needs_required_by_update = deps
886        .get(&dep_name)
887        .is_none_or(|e| !e.required_by.contains(&parent_name.to_string()));
888
889    // Update or create dependency entry
890    let entry = deps.entry(dep_name.clone()).or_insert_with(|| Dependency {
891        name: dep_name.clone(),
892        version_req: dep.version_req.clone(),
893        status: dep.status.clone(),
894        source: dep.source.clone(),
895        required_by: vec![parent_name.to_string()],
896        depends_on: Vec::new(),
897        is_core: dep.is_core,
898        is_system: dep.is_system,
899    });
900
901    // Update required_by (add the parent if not already present)
902    if needs_required_by_update {
903        entry.required_by.push(parent_name.to_string());
904    }
905
906    // Merge status (keep worst)
907    // But never overwrite a Conflict status - conflicts take precedence
908    if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
909        let existing_priority = dependency_priority(&entry.status);
910        let new_priority = dependency_priority(&dep.status);
911        if new_priority < existing_priority {
912            entry.status = dep.status.clone();
913        }
914    }
915
916    // Merge version requirements (keep more restrictive)
917    // But never overwrite a Conflict status - conflicts take precedence
918    if !dep.version_req.is_empty() && dep.version_req != entry.version_req {
919        // If entry is already a conflict, don't overwrite it with dependency status
920        if matches!(entry.status, DependencyStatus::Conflict { .. }) {
921            // Still update version if needed, but keep conflict status
922            if entry.version_req.is_empty() {
923                entry.version_req.clone_from(&dep.version_req);
924            }
925            return;
926        }
927
928        if entry.version_req.is_empty() {
929            entry.version_req.clone_from(&dep.version_req);
930        } else {
931            // Check which version requirement is more restrictive
932            let existing_status = determine_status(
933                &entry.name,
934                &entry.version_req,
935                installed,
936                provided,
937                upgradable,
938            );
939            let new_status = determine_status(
940                &entry.name,
941                &dep.version_req,
942                installed,
943                provided,
944                upgradable,
945            );
946            let existing_req_priority = dependency_priority(&existing_status);
947            let new_req_priority = dependency_priority(&new_status);
948
949            if new_req_priority < existing_req_priority {
950                entry.version_req.clone_from(&dep.version_req);
951                entry.status = new_status;
952            }
953        }
954    }
955}
956
957/// Dependency resolver for batch package operations.
958///
959/// Provides a high-level API for resolving dependencies for multiple packages,
960/// handling batch operations, conflict detection, and dependency merging.
961pub struct DependencyResolver {
962    /// Resolver configuration.
963    config: ResolverConfig,
964}
965
966impl DependencyResolver {
967    /// What: Create a new dependency resolver with default configuration.
968    ///
969    /// Inputs:
970    /// - (none)
971    ///
972    /// Output:
973    /// - Returns a new `DependencyResolver` with default configuration.
974    ///
975    /// Details:
976    /// - Uses `ResolverConfig::default()` for configuration.
977    /// - Default config: direct dependencies only, no optional/make/check deps, no AUR checking.
978    ///
979    /// # Example
980    ///
981    /// ```no_run
982    /// use arch_toolkit::deps::DependencyResolver;
983    ///
984    /// let resolver = DependencyResolver::new();
985    /// ```
986    #[must_use]
987    pub fn new() -> Self {
988        Self {
989            config: ResolverConfig::default(),
990        }
991    }
992
993    /// What: Create a resolver with custom configuration.
994    ///
995    /// Inputs:
996    /// - `config`: Custom resolver configuration.
997    ///
998    /// Output:
999    /// - Returns a new `DependencyResolver` with the provided configuration.
1000    ///
1001    /// Details:
1002    /// - Allows customization of dependency resolution behavior.
1003    ///
1004    /// # Example
1005    ///
1006    /// ```no_run
1007    /// use arch_toolkit::deps::DependencyResolver;
1008    /// use arch_toolkit::types::dependency::ResolverConfig;
1009    ///
1010    /// let config = ResolverConfig {
1011    ///     include_optdepends: true,
1012    ///     include_makedepends: false,
1013    ///     include_checkdepends: false,
1014    ///     max_depth: 0,
1015    ///     pkgbuild_cache: None,
1016    ///     check_aur: false,
1017    /// };
1018    /// let resolver = DependencyResolver::with_config(config);
1019    /// ```
1020    #[must_use]
1021    #[allow(clippy::missing_const_for_fn)] // ResolverConfig contains function pointer, can't be const
1022    pub fn with_config(config: ResolverConfig) -> Self {
1023        Self { config }
1024    }
1025
1026    /// What: Resolve dependencies for a list of packages.
1027    ///
1028    /// Inputs:
1029    /// - `packages`: Slice of `PackageRef` records to resolve dependencies for.
1030    ///
1031    /// Output:
1032    /// - Returns `Ok(DependencyResolution)` with resolved dependencies, conflicts, and missing packages.
1033    /// - Returns `Err(ArchToolkitError)` if resolution fails.
1034    ///
1035    /// Details:
1036    /// - Resolves ONLY direct dependencies (non-recursive) for each package.
1037    /// - Merges duplicates by name, retaining the most severe status across all requesters.
1038    /// - Detects conflicts between packages being installed and already installed packages.
1039    /// - Sorts dependencies by priority (conflicts first, then missing, then to-install, then installed).
1040    /// - Uses batch fetching for official packages to reduce pacman command overhead.
1041    ///
1042    /// # Errors
1043    ///
1044    /// Returns `Err(ArchToolkitError::Parse)` if pacman commands fail or output cannot be parsed.
1045    /// Returns `Err(ArchToolkitError::PackageNotFound)` if required packages are not found.
1046    ///
1047    /// # Example
1048    ///
1049    /// ```no_run
1050    /// use arch_toolkit::deps::DependencyResolver;
1051    /// use arch_toolkit::{PackageRef, PackageSource};
1052    ///
1053    /// let resolver = DependencyResolver::new();
1054    /// let packages = vec![
1055    ///     PackageRef {
1056    ///         name: "firefox".into(),
1057    ///         version: "121.0".into(),
1058    ///         source: PackageSource::Official {
1059    ///             repo: "extra".into(),
1060    ///             arch: "x86_64".into(),
1061    ///         },
1062    ///     },
1063    /// ];
1064    ///
1065    /// let result = resolver.resolve(&packages)?;
1066    /// println!("Found {} dependencies", result.dependencies.len());
1067    /// # Ok::<(), arch_toolkit::error::ArchToolkitError>(())
1068    /// ```
1069    pub fn resolve(
1070        &self,
1071        packages: &[PackageRef],
1072    ) -> Result<crate::types::dependency::DependencyResolution> {
1073        use crate::types::dependency::DependencyResolution;
1074
1075        if packages.is_empty() {
1076            tracing::warn!("No packages provided for dependency resolution");
1077            return Ok(DependencyResolution::default());
1078        }
1079
1080        let mut deps: HashMap<String, Dependency> = HashMap::new();
1081        let mut conflicts: Vec<String> = Vec::new();
1082        let mut missing: Vec<String> = Vec::new();
1083
1084        // Get installed packages set
1085        tracing::info!("Fetching list of installed packages...");
1086        let installed = get_installed_packages()?;
1087        tracing::info!("Found {} installed packages", installed.len());
1088
1089        // Get all provided packages (e.g., rustup provides rust)
1090        // Note: Provides are checked lazily on-demand for performance, not built upfront
1091        tracing::debug!(
1092            "Provides will be checked lazily on-demand (not building full set for performance)"
1093        );
1094        let provided = get_provided_packages(&installed);
1095
1096        // Get list of upgradable packages to detect if dependencies need upgrades
1097        let upgradable = get_upgradable_packages()?;
1098        tracing::info!("Found {} upgradable packages", upgradable.len());
1099
1100        // Initialize set of root packages (for tracking)
1101        let root_names: HashSet<String> = packages.iter().map(|p| p.name.clone()).collect();
1102
1103        // Check conflicts for packages being installed
1104        tracing::info!("Checking conflicts for {} package(s)", packages.len());
1105        for package in packages {
1106            let package_conflicts = fetch_package_conflicts(&package.name, &package.source);
1107            for conflict_name in package_conflicts {
1108                if installed.contains(&conflict_name) || root_names.contains(&conflict_name) {
1109                    if !conflicts.contains(&conflict_name) {
1110                        conflicts.push(conflict_name.clone());
1111                    }
1112                    // Mark as conflict in dependency map
1113                    let dep = Dependency {
1114                        name: conflict_name,
1115                        version_req: String::new(),
1116                        status: DependencyStatus::Conflict {
1117                            reason: format!("Conflicts with {}", package.name),
1118                        },
1119                        source: DependencySource::Local,
1120                        required_by: vec![package.name.clone()],
1121                        depends_on: Vec::new(),
1122                        is_core: false,
1123                        is_system: false,
1124                    };
1125                    merge_dependency(
1126                        &dep,
1127                        &package.name,
1128                        &installed,
1129                        &provided,
1130                        &upgradable,
1131                        &mut deps,
1132                    );
1133                }
1134            }
1135        }
1136
1137        // Batch fetch official package dependencies to reduce pacman command overhead
1138        let official_packages: Vec<&str> = packages
1139            .iter()
1140            .filter_map(|pkg| {
1141                if let PackageSource::Official { repo, .. } = &pkg.source {
1142                    if repo == "local" {
1143                        None
1144                    } else {
1145                        Some(pkg.name.as_str())
1146                    }
1147                } else {
1148                    None
1149                }
1150            })
1151            .collect();
1152        let batched_deps_cache = if official_packages.is_empty() {
1153            HashMap::new()
1154        } else {
1155            batch_fetch_official_deps(&official_packages)
1156        };
1157
1158        // Resolve ONLY direct dependencies (non-recursive)
1159        // This is faster and avoids resolving transitive dependencies which can be slow and error-prone
1160        for package in packages {
1161            // Check if we have batched results for this official package
1162            let use_batched = matches!(package.source, PackageSource::Official { ref repo, .. } if repo != "local")
1163                && batched_deps_cache.contains_key(package.name.as_str());
1164
1165            let resolved_deps = if use_batched {
1166                // Use batched dependency list
1167                let dep_names = batched_deps_cache
1168                    .get(package.name.as_str())
1169                    .cloned()
1170                    .unwrap_or_default();
1171                process_dependency_specs(
1172                    dep_names,
1173                    &package.name,
1174                    &installed,
1175                    &provided,
1176                    &upgradable,
1177                )
1178            } else {
1179                // Resolve individually
1180                match resolve_package_deps(
1181                    &package.name,
1182                    &package.source,
1183                    &installed,
1184                    &provided,
1185                    &upgradable,
1186                    self.config
1187                        .pkgbuild_cache
1188                        .as_ref()
1189                        .map(|f| f.as_ref() as &(dyn Fn(&str) -> Option<String> + Send + Sync)),
1190                ) {
1191                    Ok(deps) => deps,
1192                    Err(e) => {
1193                        tracing::warn!(
1194                            "  Failed to resolve dependencies for {}: {}",
1195                            package.name,
1196                            e
1197                        );
1198                        // Mark as missing
1199                        if !missing.contains(&package.name) {
1200                            missing.push(package.name.clone());
1201                        }
1202                        continue;
1203                    }
1204                }
1205            };
1206
1207            tracing::debug!(
1208                "  Found {} dependencies for {}",
1209                resolved_deps.len(),
1210                package.name
1211            );
1212
1213            for dep in resolved_deps {
1214                // Check if dependency is missing
1215                if matches!(dep.status, DependencyStatus::Missing) && !missing.contains(&dep.name) {
1216                    missing.push(dep.name.clone());
1217                }
1218
1219                merge_dependency(
1220                    &dep,
1221                    &package.name,
1222                    &installed,
1223                    &provided,
1224                    &upgradable,
1225                    &mut deps,
1226                );
1227
1228                // DON'T recursively resolve dependencies - only show direct dependencies
1229                // This prevents resolving transitive dependencies which can be slow and error-prone
1230            }
1231        }
1232
1233        let mut result: Vec<Dependency> = deps.into_values().collect();
1234        tracing::info!("Total unique dependencies found: {}", result.len());
1235
1236        // Sort dependencies: conflicts first, then missing, then to-install, then installed
1237        result.sort_by(|a, b| {
1238            let priority_a = dependency_priority(&a.status);
1239            let priority_b = dependency_priority(&b.status);
1240            priority_a
1241                .cmp(&priority_b)
1242                .then_with(|| a.name.cmp(&b.name))
1243        });
1244
1245        Ok(DependencyResolution {
1246            dependencies: result,
1247            conflicts,
1248            missing,
1249        })
1250    }
1251}
1252
1253impl Default for DependencyResolver {
1254    fn default() -> Self {
1255        Self::new()
1256    }
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261    use super::*;
1262    use crate::types::dependency::DependencyStatus;
1263
1264    #[test]
1265    fn test_should_filter_dependency() {
1266        assert!(should_filter_dependency("libfoo.so", "package"));
1267        assert!(should_filter_dependency("libfoo.so.1", "package"));
1268        assert!(should_filter_dependency("libfoo.so=1", "package"));
1269        assert!(should_filter_dependency("package", "package")); // self-reference
1270        assert!(!should_filter_dependency("glibc", "package"));
1271        assert!(!should_filter_dependency("firefox", "package"));
1272    }
1273
1274    #[test]
1275    fn test_determine_status_not_installed() {
1276        let installed = HashSet::new();
1277        let provided = HashSet::new();
1278        let upgradable = HashSet::new();
1279
1280        let status = determine_status("nonexistent", "", &installed, &provided, &upgradable);
1281        assert!(matches!(status, DependencyStatus::ToInstall));
1282    }
1283
1284    #[test]
1285    fn test_batch_fetch_official_deps_parsing() {
1286        // Test parsing logic with sample output
1287        let sample_output = "Name            : firefox\nDepends On      : glibc\n\nName            : vim\nDepends On      : glibc\n";
1288        let mut package_blocks = Vec::new();
1289        let mut current_block = String::new();
1290        for line in sample_output.lines() {
1291            if line.trim().is_empty() {
1292                if !current_block.is_empty() {
1293                    package_blocks.push(current_block.clone());
1294                    current_block.clear();
1295                }
1296            } else {
1297                current_block.push_str(line);
1298                current_block.push('\n');
1299            }
1300        }
1301        if !current_block.is_empty() {
1302            package_blocks.push(current_block);
1303        }
1304
1305        assert_eq!(package_blocks.len(), 2);
1306        assert!(package_blocks[0].contains("firefox"));
1307        assert!(package_blocks[1].contains("vim"));
1308    }
1309
1310    #[test]
1311    fn test_dependency_priority() {
1312        assert_eq!(
1313            dependency_priority(&DependencyStatus::Conflict {
1314                reason: "test".to_string(),
1315            }),
1316            0
1317        );
1318        assert_eq!(dependency_priority(&DependencyStatus::Missing), 1);
1319        assert_eq!(dependency_priority(&DependencyStatus::ToInstall), 2);
1320        assert_eq!(
1321            dependency_priority(&DependencyStatus::ToUpgrade {
1322                current: "1.0".to_string(),
1323                required: "2.0".to_string(),
1324            }),
1325            3
1326        );
1327        assert_eq!(
1328            dependency_priority(&DependencyStatus::Installed {
1329                version: "1.0".to_string(),
1330            }),
1331            4
1332        );
1333    }
1334
1335    #[test]
1336    fn test_dependency_resolver_new() {
1337        let resolver = DependencyResolver::new();
1338        // Just verify it can be created
1339        assert!(matches!(resolver.config.max_depth, 0));
1340    }
1341
1342    #[test]
1343    fn test_dependency_resolver_with_config() {
1344        let config = ResolverConfig {
1345            include_optdepends: true,
1346            include_makedepends: true,
1347            include_checkdepends: true,
1348            max_depth: 2,
1349            pkgbuild_cache: None,
1350            check_aur: true,
1351        };
1352        let resolver = DependencyResolver::with_config(config);
1353        assert_eq!(resolver.config.max_depth, 2);
1354        assert!(resolver.config.include_optdepends);
1355        assert!(resolver.config.check_aur);
1356    }
1357
1358    #[test]
1359    fn test_dependency_resolver_resolve_empty() {
1360        let resolver = DependencyResolver::new();
1361        let result = resolver
1362            .resolve(&[])
1363            .expect("resolve should succeed for empty packages");
1364        assert_eq!(result.dependencies.len(), 0);
1365        assert_eq!(result.conflicts.len(), 0);
1366        assert_eq!(result.missing.len(), 0);
1367    }
1368
1369    // Integration tests that require pacman - these are ignored by default
1370    #[test]
1371    #[ignore = "Requires pacman to be available"]
1372    fn test_dependency_resolver_resolve_integration() {
1373        let resolver = DependencyResolver::new();
1374        let packages = vec![PackageRef {
1375            name: "pacman".to_string(),
1376            version: "6.1.0".to_string(),
1377            source: PackageSource::Official {
1378                repo: "core".to_string(),
1379                arch: "x86_64".to_string(),
1380            },
1381        }];
1382
1383        if let Ok(result) = resolver.resolve(&packages) {
1384            // Should find some dependencies for pacman
1385            println!("Found {} dependencies", result.dependencies.len());
1386            assert!(!result.dependencies.is_empty());
1387        }
1388    }
1389}