use anyhow::Result;
use console::style;

use crate::exec;

/// Where a package was found.
#[derive(Debug, Clone, PartialEq)]
pub enum Source {
    Nix,
    BrewCask,
    BrewFormula,
}

impl std::fmt::Display for Source {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Source::Nix => write!(f, "nixpkgs"),
            Source::BrewCask => write!(f, "brew cask"),
            Source::BrewFormula => write!(f, "brew formula"),
        }
    }
}

/// A package found in a particular source with its version.
#[derive(Debug, Clone)]
pub struct Candidate {
    pub source: Source,
    pub version: String,
}

/// The resolution outcome.
pub enum Resolution {
    /// Only one source has it — use that.
    Single(Candidate),
    /// Multiple sources — nex recommends one but the user should confirm.
    Conflict {
        candidates: Vec<Candidate>,
        recommended: Source,
        reason: String,
        /// True when nix and brew have the same version (eligible for auto-preference).
        versions_equal: bool,
    },
    /// Not found anywhere.
    NotFound,
}

/// Full result of resolve(), including whether all sources were checked.
pub struct ResolveResult {
    pub resolution: Resolution,
    /// True if brew was available and checked during resolution.
    pub brew_checked: bool,
}

/// Resolve a package across nixpkgs, brew casks, and brew formulae.
pub fn resolve(pkg: &str) -> Result<ResolveResult> {
    tracing::debug!(%pkg, "resolving package");
    let mut candidates = Vec::new();
    let brew_checked = exec::brew_available();

    // Check nixpkgs — try the canonical alias first, then the raw name
    let nix_attr = crate::aliases::nixpkgs_attr(pkg);
    let nix_version = exec::nix_eval_version(nix_attr)?.or(if nix_attr != pkg {
        exec::nix_eval_version(pkg)?
    } else {
        None
    });
    if let Some(version) = nix_version {
        tracing::debug!(%pkg, version = %version, "found in nixpkgs");
        candidates.push(Candidate {
            source: Source::Nix,
            version,
        });
    }

    if brew_checked {
        // Check brew cask — try the raw name, then known cask aliases
        let cask_version =
            exec::brew_cask_info(pkg)?.or(match crate::aliases::brew_cask_name(pkg) {
                Some(cask_name) if cask_name != pkg => exec::brew_cask_info(cask_name)?,
                _ => None,
            });
        if let Some(version) = cask_version {
            tracing::debug!(%pkg, version = %version, "found as brew cask");
            candidates.push(Candidate {
                source: Source::BrewCask,
                version,
            });
        }

        // Check brew formula (only if not found as cask — formulae and casks rarely overlap)
        if !candidates.iter().any(|c| c.source == Source::BrewCask) {
            if let Some(version) = exec::brew_formula_info(pkg)? {
                tracing::debug!(%pkg, version = %version, "found as brew formula");
                candidates.push(Candidate {
                    source: Source::BrewFormula,
                    version,
                });
            }
        }
    }

    let resolution = match candidates.len() {
        0 => Resolution::NotFound,
        1 => {
            // Safe: len checked above
            #[allow(clippy::unwrap_used)]
            let candidate = candidates.into_iter().next().unwrap();
            Resolution::Single(candidate)
        }
        _ => {
            let (recommended, reason, versions_equal) = recommend(&candidates);
            Resolution::Conflict {
                candidates,
                recommended,
                reason,
                versions_equal,
            }
        }
    };

    Ok(ResolveResult {
        resolution,
        brew_checked,
    })
}

/// Compare two version strings with semantic version awareness.
/// Falls back to string equality when both versions fail to parse as semver.
fn versions_equal(a: &str, b: &str) -> bool {
    let result = if a == b {
        true
    } else if let (Ok(va), Ok(vb)) = (
        semver::Version::parse(a).or_else(|_| lenient_semver_parse(a)),
        semver::Version::parse(b).or_else(|_| lenient_semver_parse(b)),
    ) {
        // Try lenient semver parse (handles versions like "1.0" -> "1.0.0")
        va == vb
    } else {
        false
    };
    tracing::trace!(%a, %b, equal = %result, "version comparison");
    result
}

/// Try to parse a version string that may be missing patch/minor components.
fn lenient_semver_parse(s: &str) -> Result<semver::Version, semver::Error> {
    let parts: Vec<&str> = s.split('.').collect();
    match parts.len() {
        1 => semver::Version::parse(&format!("{}.0.0", s)),
        2 => semver::Version::parse(&format!("{}.0", s)),
        _ => semver::Version::parse(s),
    }
}

/// Decide which source to recommend when there's a conflict.
/// Returns (recommended_source, reason, versions_equal).
fn recommend(candidates: &[Candidate]) -> (Source, String, bool) {
    let nix = candidates.iter().find(|c| c.source == Source::Nix);
    let brew = candidates
        .iter()
        .find(|c| c.source == Source::BrewCask || c.source == Source::BrewFormula);

    tracing::debug!(nix_ver = ?nix.map(|n| &n.version), brew_ver = ?brew.map(|b| &b.version), "comparing versions");

    if let (Some(n), Some(b)) = (nix, brew) {
        let eq = versions_equal(&n.version, &b.version);

        if b.source == Source::BrewCask && package_has_cask_redirect(&n.version) {
            return (
                Source::BrewCask,
                "nix package redirects to the Homebrew cask".into(),
                eq,
            );
        }

        if eq {
            // Same version — recommend nix (declarative, reproducible)
            return (
                Source::Nix,
                "same version in both — nix provides declarative management".into(),
                true,
            );
        }

        // Brew has a different (likely newer) version — recommend brew
        let brew_label = if b.source == Source::BrewCask {
            "cask"
        } else {
            "formula"
        };
        return (
            b.source.clone(),
            format!("brew {brew_label} is {} (nix has {})", b.version, n.version),
            false,
        );
    }

    // Fallback: prefer nix
    (
        Source::Nix,
        "nix provides declarative management".into(),
        false,
    )
}

fn package_has_cask_redirect(version: &str) -> bool {
    let lower = version.to_ascii_lowercase();
    lower.contains("use brew cask") || lower.contains("homebrew cask")
}

/// Result of an interactive resolution prompt.
pub struct PromptResult {
    pub source: Source,
    /// If true, the user wants to always pick nix for equal-version conflicts.
    pub remember_nix: bool,
}

/// Display the resolution to the user and return the chosen source.
/// Returns None if the user cancels.
/// When `versions_equal` is true, offers an "always use nix" option.
pub fn prompt_resolution(
    pkg: &str,
    candidates: &[Candidate],
    recommended: &Source,
    reason: &str,
    versions_equal: bool,
) -> Result<Option<PromptResult>> {
    eprintln!();
    eprintln!("  {} found in multiple sources:", style(pkg).bold());
    eprintln!();

    for c in candidates {
        let marker = if c.source == *recommended {
            style("*").green().bold().to_string()
        } else {
            " ".to_string()
        };
        let source_label = format!("{:<14}", c.source.to_string());
        eprintln!(
            "  {}  {}  {}",
            marker,
            style(source_label).cyan(),
            c.version
        );
    }

    eprintln!();
    eprintln!("  {} {}", style("recommended:").dim(), reason);
    eprintln!();

    // Build selection items
    let mut items: Vec<String> = candidates
        .iter()
        .map(|c| {
            let rec = if c.source == *recommended {
                " (recommended)"
            } else {
                ""
            };
            format!("{} {}{}", c.source, c.version, rec)
        })
        .collect();

    // When versions match, offer an "always nix" option
    if versions_equal {
        items.push("Always use nix when versions match".into());
    }

    let default_idx = candidates
        .iter()
        .position(|c| c.source == *recommended)
        .unwrap_or(0);

    // When not interactive, use the recommended default
    if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
        return Ok(Some(PromptResult {
            source: recommended.clone(),
            remember_nix: false,
        }));
    }

    let items_str: Vec<String> = items.iter().map(|s| s.to_string()).collect();
    let selection = crate::input::input()
        .select("Install as", &items_str, default_idx)
        .ok();

    match selection {
        Some(idx) if idx < candidates.len() => Ok(Some(PromptResult {
            source: candidates[idx].source.clone(),
            remember_nix: false,
        })),
        Some(_) => {
            // "Always use nix" option selected
            Ok(Some(PromptResult {
                source: Source::Nix,
                remember_nix: true,
            }))
        }
        None => Ok(None),
    }
}

#[cfg(test)]
mod tests {
    use super::{recommend, Candidate, Source};

    #[test]
    fn recommends_brew_cask_when_nix_version_is_cask_redirect_message() {
        let candidates = vec![
            Candidate {
                source: Source::Nix,
                version: "Please use brew cask to install kitty".to_string(),
            },
            Candidate {
                source: Source::BrewCask,
                version: "Run `brew install --cask kitty`".to_string(),
            },
        ];

        let (source, reason, versions_equal) = recommend(&candidates);

        assert_eq!(source, Source::BrewCask);
        assert!(reason.contains("brew cask"));
        assert!(!versions_equal);
    }
}
