//! Common types used throughout Rattler-Build
//! All the metadata that makes up a recipe file
use std::{iter, path::PathBuf, str::FromStr};

use jiff::Timestamp;
use rattler_conda_types::{
    Channel, ChannelUrl, GenericVirtualPackage, PackageName, Platform, VersionWithSource,
    compression_level::CompressionLevel,
    package::{CondaArchiveType, PathsJson},
};
use rattler_index::{IndexFsConfig, index_fs};
use rattler_repodata_gateway::{CacheClearMode, SubdirSelection};
use rattler_virtual_packages::{
    DetectVirtualPackageError, VirtualPackageOverrides, VirtualPackages,
};
use serde::{Deserialize, Deserializer, Serialize};

use crate::tool_configuration;

mod build_configuration;
mod build_output;
mod directories;

pub use build_configuration::BuildConfiguration;
pub use build_output::BuildOutput as Output;
pub use directories::{Directories, ExecutionDirectories, padded_host_prefix};

/// Settings when creating the package (compression etc.)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackagingSettings {
    /// The archive type, currently supported are `tar.bz2` and `conda`
    pub archive_type: CondaArchiveType,
    /// The compression level from 1-9 or -7-22 for `tar.bz2` and `conda`
    /// archives
    pub compression_level: i32,
}

impl PackagingSettings {
    /// Create a new `PackagingSettings` from the command line arguments
    /// and the selected archive type.
    pub fn from_args(archive_type: CondaArchiveType, compression_level: CompressionLevel) -> Self {
        let compression_level: i32 = match archive_type {
            CondaArchiveType::TarBz2 => compression_level.to_bzip2_level().unwrap() as i32,
            CondaArchiveType::Conda => compression_level.to_zstd_level().unwrap(),
        };

        Self {
            archive_type,
            compression_level,
        }
    }
}

/// Defines both a platform and the virtual packages that describe the
/// capabilities of the platform.
#[derive(Debug, Clone, Serialize)]
pub struct PlatformWithVirtualPackages {
    /// The platform
    pub platform: Platform,

    /// The virtual packages for the platform
    pub virtual_packages: Vec<GenericVirtualPackage>,
}

impl PlatformWithVirtualPackages {
    /// Returns the current platform and the virtual packages available on the
    /// current system.
    pub fn detect(overrides: &VirtualPackageOverrides) -> Result<Self, DetectVirtualPackageError> {
        let platform = Platform::current();
        Self::detect_for_platform(platform, overrides)
    }

    /// Detect the virtual packages for the given platform, filling in defaults where appropriate
    pub fn detect_for_platform(
        platform: Platform,
        overrides: &VirtualPackageOverrides,
    ) -> Result<Self, DetectVirtualPackageError> {
        let virtual_packages = VirtualPackages::detect_for_platform(platform, overrides)?
            .into_generic_virtual_packages()
            .collect();
        Ok(Self {
            platform,
            virtual_packages,
        })
    }
}

impl<'de> Deserialize<'de> for PlatformWithVirtualPackages {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        pub struct Object {
            pub platform: Platform,
            pub virtual_packages: Vec<GenericVirtualPackage>,
        }

        serde_untagged::UntaggedEnumVisitor::new()
            .string(|s| {
                Ok(Self {
                    platform: Platform::from_str(s).map_err(serde::de::Error::custom)?,
                    virtual_packages: vec![],
                })
            })
            .map(|m| {
                let object: Object = m.deserialize()?;
                Ok(Self {
                    platform: object.platform,
                    virtual_packages: object.virtual_packages,
                })
            })
            .deserialize(deserializer)
    }
}

/// A package identifier
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PackageIdentifier {
    /// The name of the package
    pub name: PackageName,
    /// The version of the package
    pub version: VersionWithSource,
    /// The build string of the package
    pub build_string: String,
}

/// The summary of a build
#[derive(Debug, Clone, Default)]
pub struct BuildSummary {
    /// The start time of the build
    pub build_start: Option<Timestamp>,
    /// The end time of the build
    pub build_end: Option<Timestamp>,

    /// The path to the artifact
    pub artifact: Option<PathBuf>,
    /// Any warnings that were recorded during the build
    pub warnings: Vec<String>,
    /// The paths that are packaged in the artifact
    pub paths: Option<PathsJson>,
    ///  Whether the build was successful or not
    pub failed: bool,
}

/// Builds the channel list and reindexes the output channel.
pub async fn build_reindexed_channels(
    build_configuration: &BuildConfiguration,
    tool_configuration: &tool_configuration::Configuration,
) -> Result<Vec<ChannelUrl>, std::io::Error> {
    let output_dir = &build_configuration.directories.output_dir;

    // If the output directory does not exist yet there are no locally built
    // packages to index, so there is nothing to add as a local channel. Skip it
    // and return only the configured channels. `Channel::from_directory` would
    // otherwise panic with "path is a not a valid absolute path" while trying to
    // canonicalize the missing path, which happened e.g. when running
    // `--render-only --with-solve` without a pre-existing output directory
    // (see issue #2611).
    if !output_dir.exists() {
        return Ok(build_configuration.channels.clone());
    }

    let output_channel =
        Channel::try_from_directory(output_dir).expect("could not create channel from directory");

    // Clear the repodata gateway of any cached values for the output channel.
    // Clear all subdirs so that packages from other platforms (e.g. a linux-64
    // dependency of a noarch package) are visible after reindexing.
    tool_configuration.repodata_gateway.clear_repodata_cache(
        &output_channel,
        SubdirSelection::All,
        // In memory is enough because this is a "file" channel
        CacheClearMode::InMemoryOnly,
    )?;

    let index_config = IndexFsConfig {
        channel: output_dir.clone(),
        target_platform: None,
        repodata_patch: None,
        write_zst: false,
        write_shards: false,
        repodata_revisions: Vec::new(),
        package_revision_assignment: Default::default(),
        force: false,
        max_parallel: num_cpus::get_physical(),
        multi_progress: None,
    };

    // Reindex the output channel from the files on disk
    index_fs(index_config)
        .await
        .map_err(std::io::Error::other)?;

    Ok(iter::once(output_channel.base_url)
        .chain(build_configuration.channels.iter().cloned())
        .collect())
}
