use std::collections::HashSet;

use chrono::{DateTime, Utc};
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use miette::Diagnostic;
use pixi_spec::{ExcludeNewer, ResolvedExcludeNewer};
use pixi_spec_containers::DependencyMap;
use rattler_conda_types::{
    ChannelConfig, ChannelUrl, NamedChannelOrUrl, ParseChannelError, Platform,
};

use crate::{
    CondaConstraints, CondaDependencies, PrioritizedChannel, PyPiDependencies, SpecType,
    SystemRequirements,
    dependencies::CondaDevDependencies,
    has_features_iter::HasFeaturesIter,
    has_manifest_ref::HasWorkspaceManifest,
    pypi::{ResolvedPypiExcludeNewer, pypi_options::PypiOptions},
    workspace::{ChannelPriority, SolveStrategy},
};

/// ChannelPriorityCombination error, thrown when multiple channel priorities
/// are set
#[derive(Debug, thiserror::Error, Diagnostic)]
#[error("Multiple channel priorities are not allowed in a single environment")]
pub struct ChannelPriorityCombinationError;

/// A trait that implement various methods for collections that combine
/// attributes of Features It is implemented by Environment, GroupedEnvironment
/// and SolveGroup. Remove some of the boilerplate of combining features and its
/// derived data from multiple sources.
///
/// The name of the lifetime parameter is named `'source` that the borrow comes
/// from the source of the data for most implementations this will be the pixi
/// project.
///
/// There is blanket implementation available for all types that implement
/// [`HasWorkspaceManifest`] and [`HasFeaturesIter`]
pub trait FeaturesExt<'source>: HasWorkspaceManifest<'source> + HasFeaturesIter<'source> {
    /// Returns the channels associated with this collection, preserving the
    /// effective metadata for the first winning entry of every unique channel.
    fn prioritized_channels(
        &self,
    ) -> IndexMap<&'source NamedChannelOrUrl, &'source PrioritizedChannel> {
        let channels = self.features().flat_map(|feature| match &feature.channels {
            Some(channels) => channels,
            None => &self.workspace_manifest().workspace.channels,
        });

        let mut prioritized = IndexMap::new();
        for channel in channels.into_iter().sorted_by(|a, b| {
            let a = a.priority.unwrap_or(0);
            let b = b.priority.unwrap_or(0);
            b.cmp(&a)
        }) {
            prioritized.entry(&channel.channel).or_insert(channel);
        }

        prioritized
    }

    /// Returns the channels associated with this collection.
    ///
    /// Users can specify custom channels on a per-feature basis. This method
    /// collects and deduplicates all the channels from all the features in
    /// the order they are defined in the manifest.
    ///
    /// If a feature does not specify any channel the default channels from the
    /// project metadata are used instead.
    fn channels(&self) -> IndexSet<&'source NamedChannelOrUrl> {
        self.prioritized_channels().into_keys().collect()
    }

    /// Returns the channels associated with this collection.
    ///
    /// This function is similar to [`Self::channels]` but it resolves the
    /// channel urls using the provided channel config.
    fn channel_urls(
        &self,
        channel_config: &ChannelConfig,
    ) -> Result<Vec<ChannelUrl>, ParseChannelError> {
        self.channels()
            .into_iter()
            .cloned()
            .map(|channel| channel.into_base_url(channel_config))
            .collect()
    }

    /// Returns the channel priority, error on multiple, different values, return None if
    /// no value is set.
    ///
    /// When using multiple channel priorities over different features we should
    /// error as the user should decide what they want.
    fn channel_priority(&self) -> Result<Option<ChannelPriority>, ChannelPriorityCombinationError> {
        let mut channel_priority = None;
        for feature in self.features() {
            if let Some(priority) = feature.channel_priority {
                // If we already have a priority and it's different, error
                if channel_priority.is_some() && channel_priority != Some(priority) {
                    return Err(ChannelPriorityCombinationError);
                }
                channel_priority = Some(priority);
            }
        }
        Ok(channel_priority)
    }

    /// Returns the raw workspace exclude-newer configuration before channel and
    /// package-specific overrides are applied.
    fn exclude_newer_raw(&self) -> Option<ExcludeNewer> {
        self.workspace_manifest().workspace.exclude_newer
    }

    /// Returns the effective exclude-newer solver configuration.
    fn exclude_newer_config(
        &self,
    ) -> Result<Option<rattler_solve::ExcludeNewer>, ParseChannelError> {
        self.exclude_newer_config_resolved()
            .map(|exclude_newer| exclude_newer.map(Into::into))
    }

    /// Returns the effective exclude-newer solver configuration with absolute cutoffs.
    fn exclude_newer_config_resolved(
        &self,
    ) -> Result<Option<ResolvedExcludeNewer>, ParseChannelError> {
        exclude_newer_config_resolved_impl(self, |channel| Ok(channel.channel.to_string()))
    }

    /// Returns the effective exclude-newer configuration keyed by resolved channel URLs.
    fn exclude_newer_config_resolved_with_channel_config(
        &self,
        channel_config: &ChannelConfig,
    ) -> Result<Option<ResolvedExcludeNewer>, ParseChannelError> {
        exclude_newer_config_resolved_impl(self, |channel| {
            Ok(channel
                .channel
                .clone()
                .into_base_url(channel_config)?
                .to_string())
        })
    }

    /// Returns the effective PyPI exclude-newer solver configuration with absolute cutoffs.
    fn pypi_exclude_newer_config_resolved(&self) -> ResolvedPypiExcludeNewer {
        let mut exclude_newer = self
            .exclude_newer_raw()
            .map(|config| ResolvedPypiExcludeNewer::from_datetime(config.cutoff()))
            .unwrap_or_default();

        for (name, package_exclude_newer) in &self
            .workspace_manifest()
            .workspace
            .pypi_exclude_newer_package_overrides
        {
            exclude_newer = match package_exclude_newer {
                ExcludeNewer::Timestamp(dt) => {
                    exclude_newer.with_package_cutoff(name.as_normalized().clone(), *dt)
                }
                ExcludeNewer::Duration(duration) => exclude_newer.with_package_cutoff(
                    name.as_normalized().clone(),
                    ExcludeNewer::Duration(*duration).cutoff(),
                ),
            };
        }

        exclude_newer
    }

    /// Returns the strategy for solving packages.
    ///
    /// The chosen strategy is the first explicitly declared one in a feature
    /// as they are provided by the [`HasFeaturesIter::features`] iterator.
    ///
    /// If no feature declares a strategy, the default value of [`SolveStrategy`] is used.
    fn solve_strategy(&self) -> SolveStrategy {
        self.features()
            .flat_map(|feature| feature.solve_strategy)
            .next()
            .unwrap_or_default()
    }

    /// Returns the platforms that this collection is compatible with.
    ///
    /// Which platforms a collection support depends on which platforms the
    /// selected features of the collection supports. The platforms that are
    /// supported by the collection is the intersection of the platforms
    /// supported by its features.
    ///
    /// Features can specify which platforms they support through the
    /// `platforms` key. If a feature does not specify any platforms the
    /// features defined by the project are used.
    fn platforms(&self) -> HashSet<Platform> {
        self.features()
            .map(|feature| {
                match &feature.platforms {
                    Some(platforms) => platforms,
                    None => &self.workspace_manifest().workspace.platforms,
                }
                .iter()
                .copied()
                .collect::<HashSet<_>>()
            })
            .reduce(|accumulated_platforms, feat| {
                accumulated_platforms.intersection(&feat).copied().collect()
            })
            .unwrap_or_default()
    }

    /// Returns the system requirements for this collection.
    ///
    /// The system requirements of the collection are the union of the system
    /// requirements of all the features in the collection. If multiple
    /// features specify a requirement for the same system package, the
    /// highest is chosen.
    fn local_system_requirements(&self) -> SystemRequirements {
        self.features()
            .map(|feature| &feature.system_requirements)
            .fold(SystemRequirements::default(), |acc, req| {
                acc.union(req)
                    .expect("system requirements should have been validated upfront")
            })
    }

    /// Returns true if any of the features has any reference to a pypi
    /// dependency.
    fn has_pypi_dependencies(&self) -> bool {
        self.features().any(|f| f.has_pypi_dependencies())
    }

    /// Returns the PyPi dependencies to install for this collection.
    ///
    /// The dependencies of all features are combined. This means that if two
    /// features define a requirement for the same package that both
    /// requirements are returned. The different requirements per package
    /// are sorted in the same order as the features they came from.
    fn pypi_dependencies(&self, platform: Option<Platform>) -> PyPiDependencies {
        let deps: Vec<_> = self
            .features()
            .filter(|f| f.supports_platform(platform))
            .filter_map(|f| f.pypi_dependencies(platform))
            .collect();
        DependencyMap::merge_all(deps.iter().map(|d| d.as_ref()))
    }

    /// Returns the dependencies to install for this collection.
    ///
    /// The dependencies of all features are combined. This means that if two
    /// features define a requirement for the same package that both
    /// requirements are returned. The different requirements per package
    /// are sorted in the same order as the features they came from.
    ///
    /// If the `platform` is `None` no platform specific dependencies are taken
    /// into consideration.
    fn dependencies(&self, kind: SpecType, platform: Option<Platform>) -> CondaDependencies {
        let deps: Vec<_> = self
            .features()
            .filter(|f| f.supports_platform(platform))
            .filter_map(|f| f.dependencies(kind, platform))
            .collect();
        DependencyMap::merge_all(deps.iter().map(|d| d.as_ref()))
    }

    /// Returns the combined dependencies to install for this collection.
    ///
    /// The `build` dependencies overwrite the `host` dependencies which
    /// overwrite the `run` dependencies.
    ///
    /// The dependencies of all features are combined. This means that if two
    /// features define a requirement for the same package that both
    /// requirements are returned. The different requirements per package
    /// are sorted in the same order as the features they came from.
    ///
    /// If the `platform` is `None` no platform specific dependencies are taken
    /// into consideration.
    fn combined_dependencies(&self, platform: Option<Platform>) -> CondaDependencies {
        let deps: Vec<_> = self
            .features()
            .filter(|f| f.supports_platform(platform))
            .filter_map(|f| f.combined_dependencies(platform))
            .collect();
        DependencyMap::merge_all(deps.iter().map(|d| d.as_ref()))
    }

    /// Returns the combined dev dependencies to install for this collection.
    ///
    /// Dev dependencies from all features in the group are collected and
    /// merged. If multiple features define the same dev dependency, the
    /// last one wins (later features override earlier ones).
    fn combined_dev_dependencies(&self, platform: Option<Platform>) -> CondaDevDependencies {
        let deps: Vec<_> = self
            .features()
            .filter(|f| f.supports_platform(platform))
            .filter_map(|f| f.dev_dependencies(platform))
            .collect();

        DependencyMap::merge_all(deps.iter().map(|d| d.as_ref()))
    }

    /// Returns the combined version constraints for this collection.
    ///
    /// Constraints from all features are combined. If multiple features define
    /// a constraint for the same package, all constraints are retained and the
    /// solver must satisfy all of them simultaneously.
    ///
    /// If the `platform` is `None`, no platform specific constraints are taken
    /// into consideration.
    fn combined_constraints(&self, platform: Option<Platform>) -> CondaConstraints {
        let constraints: Vec<_> = self
            .features()
            .filter(|f| f.supports_platform(platform))
            .filter_map(|f| f.constraints(platform))
            .collect();
        DependencyMap::merge_all(constraints.iter().map(|d| d.as_ref()))
    }

    /// Returns the pypi options for this collection.
    ///
    /// The pypi options of all features are combined. They will be combined in
    /// the order that they are defined in the manifest.
    /// The index-url is a special case and can only be defined once. This
    /// should have been verified beforehand.
    fn pypi_options(&self) -> PypiOptions {
        // Collect all the pypi-options from the features in one set,
        // deduplicate them and sort them on feature index, default feature comes last.
        let pypi_options: Vec<_> = self
            .features()
            .filter_map(|feature| {
                if feature.pypi_options().is_none() {
                    self.workspace_manifest().workspace.pypi_options.as_ref()
                } else {
                    feature.pypi_options()
                }
            })
            .collect();

        // Merge all the pypi options into one.
        pypi_options
            .into_iter()
            .fold(PypiOptions::default(), |acc, opts| {
                acc.union(opts)
                    .expect("merging of pypi-options should already have been checked")
            })
    }
}

impl<'source, FeatureCollection> FeaturesExt<'source> for FeatureCollection where
    FeatureCollection: HasWorkspaceManifest<'source> + HasFeaturesIter<'source>
{
}

fn exclude_newer_config_resolved_impl<'source, T, F>(
    features: &T,
    mut channel_key: F,
) -> Result<Option<ResolvedExcludeNewer>, ParseChannelError>
where
    T: FeaturesExt<'source> + ?Sized,
    F: FnMut(&PrioritizedChannel) -> Result<String, ParseChannelError>,
{
    let mut exclude_newer = features
        .exclude_newer_raw()
        .map(|config| ResolvedExcludeNewer::from_datetime(config.cutoff()));

    for channel in features.prioritized_channels().into_values() {
        let Some(channel_exclude_newer) = channel.exclude_newer else {
            continue;
        };

        let channel_key = channel_key(channel)?;
        let config = exclude_newer
            .get_or_insert_with(|| ResolvedExcludeNewer::from_datetime(DateTime::<Utc>::MAX_UTC));

        *config = match channel_exclude_newer {
            ExcludeNewer::Timestamp(dt) => config.clone().with_channel_cutoff(channel_key, dt),
            ExcludeNewer::Duration(duration) => config
                .clone()
                .with_channel_cutoff(channel_key, ExcludeNewer::Duration(duration).cutoff()),
        };
    }

    for (name, package_exclude_newer) in &features
        .workspace_manifest()
        .workspace
        .exclude_newer_package_overrides
    {
        let config = exclude_newer
            .get_or_insert_with(|| ResolvedExcludeNewer::from_datetime(DateTime::<Utc>::MAX_UTC));

        *config = match package_exclude_newer {
            ExcludeNewer::Timestamp(dt) => config.clone().with_package_cutoff(name.clone(), *dt),
            ExcludeNewer::Duration(duration) => config
                .clone()
                .with_package_cutoff(name.clone(), ExcludeNewer::Duration(*duration).cutoff()),
        };
    }

    Ok(exclude_newer)
}
