use std::borrow::Cow;
use std::fmt::{Display, Formatter};

use uv_git_types::{GitLfs, GitReference};
use uv_normalize::ExtraName;
use uv_pep508::{MarkerEnvironment, MarkerTree, UnnamedRequirement};
use uv_pypi_types::{Hashes, ParsedUrl};

use crate::{Requirement, RequirementSource, VerbatimParsedUrl};

/// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only
/// hashes but in the future also editable and similar information.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct NameRequirementSpecification {
    /// The actual requirement.
    pub requirement: Requirement,
    /// Hashes of the downloadable packages.
    pub hashes: Vec<String>,
}

/// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only
/// hashes but in the future also editable and similar information.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct UnresolvedRequirementSpecification {
    /// The actual requirement.
    pub requirement: UnresolvedRequirement,
    /// Hashes of the downloadable packages.
    pub hashes: Vec<String>,
}

/// A requirement read from a `requirements.txt` or `pyproject.toml` file.
///
/// It is considered unresolved as we still need to query the URL for the `Unnamed` variant to
/// resolve the requirement name.
///
/// Analog to `RequirementsTxtRequirement` but with `distribution_types::Requirement` instead of
/// `uv_pep508::Requirement`.
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
pub enum UnresolvedRequirement {
    /// The uv-specific superset over PEP 508 requirements specifier incorporating
    /// `tool.uv.sources`.
    Named(Requirement),
    /// A PEP 508-like, direct URL dependency specifier.
    Unnamed(UnnamedRequirement<VerbatimParsedUrl>),
}

impl Display for UnresolvedRequirement {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Named(requirement) => write!(f, "{requirement}"),
            Self::Unnamed(requirement) => write!(f, "{requirement}"),
        }
    }
}

impl UnresolvedRequirement {
    /// Returns whether the markers apply for the given environment.
    ///
    /// When the environment is not given, this treats all marker expressions
    /// that reference the environment as true. In other words, it does
    /// environment independent expression evaluation. (Which in turn devolves
    /// to "only evaluate marker expressions that reference an extra name.")
    pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool {
        match self {
            Self::Named(requirement) => requirement.evaluate_markers(env, extras),
            Self::Unnamed(requirement) => requirement.evaluate_optional_environment(env, extras),
        }
    }

    /// Augment a user-provided requirement by attaching any specification data that was provided
    /// separately from the requirement itself (e.g., `--branch main`).
    #[must_use]
    pub fn augment_requirement(
        self,
        rev: Option<&str>,
        tag: Option<&str>,
        branch: Option<&str>,
        lfs: Option<bool>,
        marker: Option<MarkerTree>,
    ) -> Self {
        #[expect(clippy::manual_map)]
        let git_reference = if let Some(rev) = rev {
            Some(GitReference::from_rev(rev.to_string()))
        } else if let Some(tag) = tag {
            Some(GitReference::Tag(tag.to_string()))
        } else if let Some(branch) = branch {
            Some(GitReference::Branch(branch.to_string()))
        } else {
            None
        };

        match self {
            Self::Named(mut requirement) => Self::Named(Requirement {
                marker: marker
                    .map(|marker| {
                        requirement.marker.and(marker);
                        requirement.marker
                    })
                    .unwrap_or(requirement.marker),
                source: match requirement.source {
                    RequirementSource::GitDirectory {
                        git,
                        subdirectory,
                        url,
                    } => {
                        let git = if let Some(git_reference) = git_reference {
                            git.with_reference(git_reference)
                        } else {
                            git
                        };
                        let git = if let Some(lfs) = lfs {
                            git.with_lfs(GitLfs::from(lfs))
                        } else {
                            git
                        };
                        RequirementSource::GitDirectory {
                            git,
                            subdirectory,
                            url,
                        }
                    }
                    RequirementSource::GitPath {
                        git,
                        install_path,
                        ext,
                        url,
                    } => {
                        let git = if let Some(git_reference) = git_reference {
                            git.with_reference(git_reference)
                        } else {
                            git
                        };
                        let git = if let Some(lfs) = lfs {
                            git.with_lfs(GitLfs::from(lfs))
                        } else {
                            git
                        };
                        RequirementSource::GitPath {
                            git,
                            install_path,
                            ext,
                            url,
                        }
                    }
                    _ => requirement.source,
                },
                ..requirement
            }),
            Self::Unnamed(mut requirement) => Self::Unnamed(UnnamedRequirement {
                marker: marker
                    .map(|marker| {
                        requirement.marker.and(marker);
                        requirement.marker
                    })
                    .unwrap_or(requirement.marker),
                url: match requirement.url.parsed_url {
                    ParsedUrl::GitDirectory(mut git) => {
                        if let Some(git_reference) = git_reference {
                            git.url = git.url.with_reference(git_reference);
                        }
                        if let Some(lfs) = lfs {
                            git.url = git.url.with_lfs(GitLfs::from(lfs));
                        }
                        VerbatimParsedUrl {
                            parsed_url: ParsedUrl::GitDirectory(git),
                            verbatim: requirement.url.verbatim,
                        }
                    }
                    ParsedUrl::GitPath(mut git) => {
                        if let Some(git_reference) = git_reference {
                            git.url = git.url.with_reference(git_reference);
                        }
                        if let Some(lfs) = lfs {
                            git.url = git.url.with_lfs(GitLfs::from(lfs));
                        }
                        VerbatimParsedUrl {
                            parsed_url: ParsedUrl::GitPath(git),
                            verbatim: requirement.url.verbatim,
                        }
                    }
                    _ => requirement.url,
                },
                ..requirement
            }),
        }
    }

    /// Returns the extras for the requirement.
    pub fn extras(&self) -> &[ExtraName] {
        match self {
            Self::Named(requirement) => &requirement.extras,
            Self::Unnamed(requirement) => &requirement.extras,
        }
    }

    /// Return the version specifier or URL for the requirement.
    pub fn source(&self) -> Cow<'_, RequirementSource> {
        match self {
            Self::Named(requirement) => Cow::Borrowed(&requirement.source),
            Self::Unnamed(requirement) => Cow::Owned(RequirementSource::from_parsed_url(
                requirement.url.parsed_url.clone(),
                requirement.url.verbatim.clone(),
            )),
        }
    }

    /// Returns `true` if the requirement is editable.
    pub fn is_editable(&self) -> bool {
        match self {
            Self::Named(requirement) => requirement.is_editable(),
            Self::Unnamed(requirement) => requirement.url.is_editable(),
        }
    }

    /// Return the hashes of the requirement, as specified in the URL fragment.
    pub fn hashes(&self) -> Option<Hashes> {
        match self {
            Self::Named(requirement) => requirement.hashes(),
            Self::Unnamed(requirement) => {
                let fragment = requirement.url.verbatim.fragment()?;
                Hashes::parse_fragment(fragment).ok()
            }
        }
    }
}

impl NameRequirementSpecification {
    /// Return the hashes of the requirement, as specified in the URL fragment.
    pub fn hashes(&self) -> Option<Hashes> {
        let RequirementSource::Url { ref url, .. } = self.requirement.source else {
            return None;
        };
        let fragment = url.fragment()?;
        Hashes::parse_fragment(fragment).ok()
    }
}

impl From<Requirement> for UnresolvedRequirementSpecification {
    fn from(requirement: Requirement) -> Self {
        Self {
            requirement: UnresolvedRequirement::Named(requirement),
            hashes: Vec::new(),
        }
    }
}

impl From<Requirement> for NameRequirementSpecification {
    fn from(requirement: Requirement) -> Self {
        Self {
            requirement,
            hashes: Vec::new(),
        }
    }
}
