use std::collections::BTreeMap;
use std::path::Path;

use uv_auth::CredentialsCache;
use uv_cache::Cache;
use uv_configuration::NoSources;
use uv_distribution_types::{
    ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
};
use uv_normalize::PackageName;
use uv_workspace::pyproject::{ExtraBuildDependencies, ExtraBuildDependency, ToolUvSources};
use uv_workspace::{
    DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
};

use crate::metadata::{LoweredRequirement, MetadataError};

/// Lowered requirements from a `[build-system.requires]` field in a `pyproject.toml` file.
#[derive(Debug, Clone)]
pub struct BuildRequires {
    pub name: Option<PackageName>,
    pub requires_dist: Vec<Requirement>,
}

impl BuildRequires {
    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
    /// dependencies.
    fn from_metadata23(metadata: uv_pypi_types::BuildRequires) -> Self {
        Self {
            name: metadata.name,
            requires_dist: metadata
                .requires_dist
                .into_iter()
                .map(Requirement::from)
                .collect(),
        }
    }

    /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
    /// dependencies.
    pub async fn from_project_maybe_workspace(
        metadata: uv_pypi_types::BuildRequires,
        install_path: &Path,
        locations: &IndexLocations,
        sources: &NoSources,
        editable: bool,
        stop_discovery_at: Option<&Path>,
        cache: &Cache,
        workspace_cache: &WorkspaceCache,
        credentials_cache: &CredentialsCache,
    ) -> Result<Self, MetadataError> {
        let discovery = DiscoveryOptions {
            stop_discovery_at: stop_discovery_at.map(Path::to_path_buf),
            members: if sources.all() {
                MemberDiscovery::None
            } else {
                MemberDiscovery::default()
            },
        };
        let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root(
            install_path,
            &discovery,
            cache,
            workspace_cache,
        )
        .await?
        else {
            return Ok(Self::from_metadata23(metadata));
        };

        Self::from_project_workspace(
            metadata,
            &project_workspace,
            locations,
            sources,
            editable,
            cache,
            workspace_cache,
            credentials_cache,
        )
        .await
    }

    /// Lower the `build-system.requires` field from a `pyproject.toml` file.
    async fn from_project_workspace(
        metadata: uv_pypi_types::BuildRequires,
        project_workspace: &ProjectWorkspace,
        locations: &IndexLocations,
        sources: &NoSources,
        editable: bool,
        cache: &Cache,
        workspace_cache: &WorkspaceCache,
        credentials_cache: &CredentialsCache,
    ) -> Result<Self, MetadataError> {
        // Collect any `tool.uv.index` entries.
        let empty = vec![];
        let project_indexes = if sources.all() {
            &empty
        } else {
            project_workspace
                .current_project()
                .pyproject_toml()
                .tool
                .as_ref()
                .and_then(|tool| tool.uv.as_ref())
                .and_then(|uv| uv.index.as_deref())
                .unwrap_or(&empty)
        };

        // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
        let empty = BTreeMap::default();
        let project_sources = if sources.all() {
            &empty
        } else {
            project_workspace
                .current_project()
                .pyproject_toml()
                .tool
                .as_ref()
                .and_then(|tool| tool.uv.as_ref())
                .and_then(|uv| uv.sources.as_ref())
                .map(ToolUvSources::inner)
                .unwrap_or(&empty)
        };

        // Lower the requirements.
        let mut requires_dist = Vec::new();
        for requirement in metadata.requires_dist {
            if sources.for_package(&requirement.name) {
                requires_dist.push(Requirement::from(requirement));
                continue;
            }

            let requirement_name = requirement.name.clone();
            let extra = requirement.marker.top_level_extra_name();
            requires_dist.extend(
                LoweredRequirement::from_requirement(
                    requirement,
                    metadata.name.as_ref(),
                    project_workspace.project_root(),
                    project_sources,
                    project_indexes,
                    extra.as_deref(),
                    None,
                    locations,
                    project_workspace.workspace(),
                    None,
                    editable,
                    cache,
                    workspace_cache,
                    credentials_cache,
                )
                .await
                .map(|requirement| {
                    requirement
                        .map(LoweredRequirement::into_inner)
                        .map_err(|err| {
                            MetadataError::LoweringError(requirement_name.clone(), Box::new(err))
                        })
                })
                .collect::<Result<Vec<_>, _>>()?,
            );
        }

        Ok(Self {
            name: metadata.name,
            requires_dist,
        })
    }

    /// Lower the `build-system.requires` field from a `pyproject.toml` file.
    pub async fn from_workspace(
        metadata: uv_pypi_types::BuildRequires,
        workspace: &Workspace,
        locations: &IndexLocations,
        sources: &NoSources,
        cache: &Cache,
        workspace_cache: &WorkspaceCache,
        credentials_cache: &CredentialsCache,
    ) -> Result<Self, MetadataError> {
        // Collect any `tool.uv.index` entries.
        let empty = vec![];
        let project_indexes = workspace
            .pyproject_toml()
            .tool
            .as_ref()
            .and_then(|tool| tool.uv.as_ref())
            .and_then(|uv| uv.index.as_deref())
            .unwrap_or(&empty);

        // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
        let empty = BTreeMap::default();
        let project_sources = workspace
            .pyproject_toml()
            .tool
            .as_ref()
            .and_then(|tool| tool.uv.as_ref())
            .and_then(|uv| uv.sources.as_ref())
            .map(ToolUvSources::inner)
            .unwrap_or(&empty);

        // Lower the requirements.
        let mut requires_dist = Vec::new();
        for requirement in metadata.requires_dist {
            if sources.for_package(&requirement.name) {
                requires_dist.push(Requirement::from(requirement));
                continue;
            }

            let requirement_name = requirement.name.clone();
            let extra = requirement.marker.top_level_extra_name();
            requires_dist.extend(
                LoweredRequirement::from_requirement(
                    requirement,
                    None,
                    workspace.install_path(),
                    project_sources,
                    project_indexes,
                    extra.as_deref(),
                    None,
                    locations,
                    workspace,
                    None,
                    true,
                    cache,
                    workspace_cache,
                    credentials_cache,
                )
                .await
                .map(|requirement| {
                    requirement
                        .map(LoweredRequirement::into_inner)
                        .map_err(|err| {
                            MetadataError::LoweringError(requirement_name.clone(), Box::new(err))
                        })
                })
                .collect::<Result<Vec<_>, _>>()?,
            );
        }

        Ok(Self {
            name: metadata.name,
            requires_dist,
        })
    }
}

/// Lowered extra build dependencies.
///
/// This is a wrapper around [`ExtraBuildRequires`] that provides methods to lower
/// [`ExtraBuildDependencies`] from a workspace context or from already lowered dependencies.
#[derive(Debug, Clone, Default)]
pub struct LoweredExtraBuildDependencies(ExtraBuildRequires);

impl LoweredExtraBuildDependencies {
    /// Return the [`ExtraBuildRequires`] that this was lowered into.
    pub fn into_inner(self) -> ExtraBuildRequires {
        self.0
    }

    /// Create from a workspace, lowering the extra build dependencies.
    pub async fn from_workspace(
        extra_build_dependencies: ExtraBuildDependencies,
        workspace: &Workspace,
        index_locations: &IndexLocations,
        source_strategy: &NoSources,
        cache: &Cache,
        workspace_cache: &WorkspaceCache,
        credentials_cache: &CredentialsCache,
    ) -> Result<Self, MetadataError> {
        match source_strategy {
            NoSources::None => {
                // Collect project sources and indexes
                let project_indexes = workspace
                    .pyproject_toml()
                    .tool
                    .as_ref()
                    .and_then(|tool| tool.uv.as_ref())
                    .and_then(|uv| uv.index.as_deref())
                    .unwrap_or(&[]);

                let empty_sources = BTreeMap::default();
                let project_sources = workspace
                    .pyproject_toml()
                    .tool
                    .as_ref()
                    .and_then(|tool| tool.uv.as_ref())
                    .and_then(|uv| uv.sources.as_ref())
                    .map(ToolUvSources::inner)
                    .unwrap_or(&empty_sources);

                // Lower each package's extra build dependencies
                let mut build_requires = ExtraBuildRequires::default();
                for (package_name, requirements) in extra_build_dependencies {
                    let mut lowered = Vec::new();
                    for ExtraBuildDependency {
                        requirement,
                        match_runtime,
                    } in requirements
                    {
                        let requirement_name = requirement.name.clone();
                        let extra = requirement.marker.top_level_extra_name();
                        lowered.extend(
                            LoweredRequirement::from_requirement(
                                requirement,
                                None,
                                workspace.install_path(),
                                project_sources,
                                project_indexes,
                                extra.as_deref(),
                                None,
                                index_locations,
                                workspace,
                                None,
                                true,
                                cache,
                                workspace_cache,
                                credentials_cache,
                            )
                            .await
                            .map(|requirement| {
                                requirement
                                    .map(|requirement| ExtraBuildRequirement {
                                        requirement: requirement.into_inner(),
                                        match_runtime,
                                    })
                                    .map_err(|err| {
                                        MetadataError::LoweringError(
                                            requirement_name.clone(),
                                            Box::new(err),
                                        )
                                    })
                            })
                            .collect::<Result<Vec<_>, _>>()?,
                        );
                    }
                    build_requires.insert(package_name, lowered);
                }
                Ok(Self(build_requires))
            }
            NoSources::All | NoSources::Packages(_) => {
                // Without source resolution, just return the dependencies as-is
                Ok(Self::from_non_lowered(extra_build_dependencies))
            }
        }
    }

    /// Create from lowered dependencies (for non-workspace contexts, like scripts).
    pub fn from_lowered(extra_build_dependencies: ExtraBuildRequires) -> Self {
        Self(extra_build_dependencies)
    }

    /// Create from unlowered dependencies (e.g., for contexts in the pip CLI).
    pub fn from_non_lowered(extra_build_dependencies: ExtraBuildDependencies) -> Self {
        Self(
            extra_build_dependencies
                .into_iter()
                .map(|(name, requirements)| {
                    (
                        name,
                        requirements
                            .into_iter()
                            .map(
                                |ExtraBuildDependency {
                                     requirement,
                                     match_runtime,
                                 }| {
                                    ExtraBuildRequirement {
                                        requirement: requirement.into(),
                                        match_runtime,
                                    }
                                },
                            )
                            .collect::<Vec<_>>(),
                    )
                })
                .collect(),
        )
    }
}
