use std::{io, sync::Arc};

use crate::{
    build::BuildBehaviour,
    config::Config,
    lockfile::{LocalPackage, LocalPackageLockType, LockfileIntegrityError},
    luarocks::luarocks_installation::LUAROCKS_VERSION,
    package::{PackageName, PackageReq},
    progress::{MultiProgress, Progress},
    project::{
        project_toml::LocalProjectTomlValidationError, Project, ProjectError, ProjectTreeError,
    },
    rockspec::Rockspec,
    tree::{self, Tree, TreeError},
};
use bon::{builder, Builder};
use itertools::Itertools;
use thiserror::Error;

use super::{Install, InstallError, PackageInstallSpec, Remove, RemoveError};

/// A rocks sync builder, for synchronising a tree with a lockfile.
#[derive(Builder)]
#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
pub struct Sync<'a> {
    #[builder(start_fn)]
    project: &'a Project,
    #[builder(start_fn)]
    config: &'a Config,

    #[builder(field)]
    extra_packages: Vec<PackageReq>,

    custom_tree: Option<&'a Tree>,

    progress: Option<Arc<Progress<MultiProgress>>>,
    /// Whether to validate the integrity of installed packages.
    validate_integrity: Option<bool>,
}

impl<State> SyncBuilder<'_, State>
where
    State: sync_builder::State,
{
    pub fn add_package(mut self, package: PackageReq) -> Self {
        self.extra_packages.push(package);
        self
    }
}

impl<State> SyncBuilder<'_, State>
where
    State: sync_builder::State + sync_builder::IsComplete,
{
    pub async fn sync_dependencies(self) -> Result<SyncReport, SyncError> {
        do_sync(self._build(), &LocalPackageLockType::Regular).await
    }

    pub async fn sync_test_dependencies(mut self) -> Result<SyncReport, SyncError> {
        let busted = PackageReq::new("busted".into(), None).unwrap();
        self = self.add_package(busted);
        do_sync(self._build(), &LocalPackageLockType::Test).await
    }

    pub async fn sync_build_dependencies(mut self) -> Result<SyncReport, SyncError> {
        if cfg!(target_family = "unix") && !self.extra_packages.is_empty() {
            let luarocks =
                PackageReq::new("luarocks".into(), Some(LUAROCKS_VERSION.into())).unwrap();
            self = self.add_package(luarocks);
        }
        do_sync(self._build(), &LocalPackageLockType::Build).await
    }
}

#[derive(Debug)]
pub struct SyncReport {
    pub(crate) added: Vec<LocalPackage>,
    pub(crate) removed: Vec<LocalPackage>,
}

#[derive(Error, Debug)]
pub enum SyncError {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
    Tree(#[from] TreeError),
    #[error(transparent)]
    Install(#[from] InstallError),
    #[error(transparent)]
    Remove(#[from] RemoveError),
    #[error("integrity error for package {0}: {1}\n")]
    Integrity(PackageName, LockfileIntegrityError),
    #[error(transparent)]
    ProjectTreeError(#[from] ProjectTreeError),
    #[error(transparent)]
    ProjectError(#[from] ProjectError),
    #[error(transparent)]
    LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
}

async fn do_sync(
    args: Sync<'_>,
    lock_type: &LocalPackageLockType,
) -> Result<SyncReport, SyncError> {
    let fallback_tree = match lock_type {
        LocalPackageLockType::Regular | LocalPackageLockType::Build => {
            args.project.tree(args.config)?
        }
        LocalPackageLockType::Test => args.project.test_tree(args.config)?,
    };
    let tree = args.custom_tree.unwrap_or(&fallback_tree);
    std::fs::create_dir_all(tree.root())?;

    let mut project_lockfile = args.project.lockfile()?.write_guard();
    let dest_lockfile = tree.lockfile()?;

    let progress = args.progress.unwrap_or(MultiProgress::new_arc());

    let packages = match lock_type {
        LocalPackageLockType::Regular => args
            .project
            .toml()
            .into_local()?
            .dependencies()
            .current_platform()
            .clone(),
        LocalPackageLockType::Build => args
            .project
            .toml()
            .into_local()?
            .build_dependencies()
            .current_platform()
            .clone(),
        LocalPackageLockType::Test => args
            .project
            .toml()
            .into_local()?
            .test_dependencies()
            .current_platform()
            .clone(),
    }
    .into_iter()
    .chain(args.extra_packages.into_iter().map_into())
    .collect_vec();

    let package_sync_spec = project_lockfile.package_sync_spec(&packages, lock_type);

    package_sync_spec
        .to_remove
        .iter()
        .for_each(|pkg| project_lockfile.remove(pkg, lock_type));

    let mut to_add: Vec<(tree::EntryType, LocalPackage)> = Vec::new();

    let mut report = SyncReport {
        added: Vec::new(),
        removed: Vec::new(),
    };
    for (id, local_package) in project_lockfile.rocks(lock_type) {
        if dest_lockfile.get(id).is_none() {
            let entry_type = if project_lockfile.is_entrypoint(&local_package.id(), lock_type) {
                tree::EntryType::Entrypoint
            } else {
                tree::EntryType::DependencyOnly
            };
            to_add.push((entry_type, local_package.clone()));
        }
    }
    for (id, local_package) in dest_lockfile.rocks() {
        if project_lockfile.get(id, lock_type).is_none() {
            report.removed.push(local_package.clone());
        }
    }

    let packages_to_install = to_add
        .iter()
        .cloned()
        .map(|(entry_type, pkg)| {
            PackageInstallSpec::new(pkg.clone().into_package_req(), entry_type)
                .build_behaviour(BuildBehaviour::Force)
                .pin(pkg.pinned())
                .opt(pkg.opt())
                .constraint(pkg.constraint())
                .build()
        })
        .collect_vec();
    report
        .added
        .extend(to_add.iter().map(|(_, pkg)| pkg).cloned());

    let package_db = project_lockfile.local_pkg_lock(lock_type).clone().into();

    Install::new(tree, args.config)
        .package_db(package_db)
        .packages(packages_to_install)
        .progress(progress.clone())
        .install()
        .await?;

    // Read the destination lockfile after installing
    let dest_lockfile = tree.lockfile()?;

    if args.validate_integrity.unwrap_or(true) {
        for (_, package) in &to_add {
            dest_lockfile
                .validate_integrity(package)
                .map_err(|err| SyncError::Integrity(package.name().clone(), err))?;
        }
    }

    let packages_to_remove = report
        .removed
        .iter()
        .cloned()
        .map(|pkg| pkg.id())
        .collect_vec();

    Remove::new(args.config)
        .packages(packages_to_remove)
        .progress(progress.clone())
        .remove()
        .await?;

    dest_lockfile.map_then_flush(|lockfile| -> Result<(), io::Error> {
        lockfile.sync(project_lockfile.local_pkg_lock(lock_type));
        Ok(())
    })?;

    if !package_sync_spec.to_add.is_empty() {
        // Install missing packages using the default package_db.
        let missing_packages = package_sync_spec.to_add.into_iter().map(|dep| {
            PackageInstallSpec::new(dep.package_req().clone(), tree::EntryType::Entrypoint)
                .build_behaviour(BuildBehaviour::Force)
                .pin(*dep.pin())
                .opt(*dep.opt())
                .maybe_source(dep.source.clone())
                .build()
        });

        let added = Install::new(tree, args.config)
            .packages(missing_packages)
            .progress(progress.clone())
            .install()
            .await?;

        report.added.extend(added);

        // Sync the newly added packages back to the project lockfile
        let dest_lockfile = tree.lockfile()?;
        project_lockfile.sync(dest_lockfile.local_pkg_lock(), lock_type);
    }

    Ok(report)
}

#[cfg(test)]
mod tests {
    use super::Sync;
    use crate::{
        config::ConfigBuilder, lockfile::LocalPackageLockType, package::PackageReq,
        project::Project,
    };
    use assert_fs::{prelude::PathCopy, TempDir};
    use std::path::PathBuf;

    #[tokio::test]
    async fn test_sync_add_rocks() {
        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
            println!("Skipping impure test");
            return;
        }
        let temp_dir = TempDir::new().unwrap();
        temp_dir
            .copy_from(
                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                    .join("resources/test/sample-project-dependencies"),
                &["**"],
            )
            .unwrap();
        let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
        let config = ConfigBuilder::new().unwrap().build().unwrap();
        let report = Sync::new(&project, &config)
            .sync_dependencies()
            .await
            .unwrap();
        assert!(report.removed.is_empty());
        assert!(!report.added.is_empty());

        let lockfile_after_sync = project.lockfile().unwrap();
        assert!(!lockfile_after_sync
            .rocks(&LocalPackageLockType::Regular)
            .is_empty());
    }

    #[tokio::test]
    async fn test_sync_add_rocks_with_new_package() {
        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
            println!("Skipping impure test");
            return;
        }
        let temp_dir = TempDir::new().unwrap();
        temp_dir
            .copy_from(
                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                    .join("resources/test/sample-project-dependencies"),
                &["**"],
            )
            .unwrap();
        let temp_dir = temp_dir.into_persistent();
        let config = ConfigBuilder::new().unwrap().build().unwrap();
        let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
        {
            let report = Sync::new(&project, &config)
                .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
                .sync_dependencies()
                .await
                .unwrap();
            assert!(report.removed.is_empty());
            assert!(!report.added.is_empty());
            assert!(report
                .added
                .iter()
                .any(|pkg| pkg.name().to_string() == "toml-edit"));
        }
        let lockfile_after_sync = project.lockfile().unwrap();
        assert!(!lockfile_after_sync
            .rocks(&LocalPackageLockType::Regular)
            .is_empty());
    }

    #[tokio::test]
    async fn regression_sync_nonexistent_lock() {
        // This test checks that we can sync a lockfile that doesn't exist yet, and whether
        // the sync report is valid.
        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
            println!("Skipping impure test");
            return;
        }
        let temp_dir = TempDir::new().unwrap();
        temp_dir
            .copy_from(
                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                    .join("resources/test/sample-project-dependencies"),
                &["**"],
            )
            .unwrap();
        let config = ConfigBuilder::new().unwrap().build().unwrap();
        let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
        {
            let report = Sync::new(&project, &config)
                .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
                .sync_dependencies()
                .await
                .unwrap();
            assert!(report.removed.is_empty());
            assert!(!report.added.is_empty());
            assert!(report
                .added
                .iter()
                .any(|pkg| pkg.name().to_string() == "toml-edit"));
        }
        let lockfile_after_sync = project.lockfile().unwrap();
        assert!(!lockfile_after_sync
            .rocks(&LocalPackageLockType::Regular)
            .is_empty());
    }

    #[tokio::test]
    async fn test_sync_remove_rocks() {
        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
            println!("Skipping impure test");
            return;
        }
        let temp_dir = TempDir::new().unwrap();
        temp_dir
            .copy_from(
                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                    .join("resources/test/sample-project-dependencies"),
                &["**"],
            )
            .unwrap();
        let config = ConfigBuilder::new().unwrap().build().unwrap();
        let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
        // First sync to create the tree and lockfile
        Sync::new(&project, &config)
            .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
            .sync_dependencies()
            .await
            .unwrap();
        let report = Sync::new(&project, &config)
            .sync_dependencies()
            .await
            .unwrap();
        assert!(!report.removed.is_empty());
        assert!(report.added.is_empty());

        let lockfile_after_sync = project.lockfile().unwrap();
        assert!(!lockfile_after_sync
            .rocks(&LocalPackageLockType::Regular)
            .is_empty());
    }
}
