use std::io;

use crate::fs;
use crate::lockfile::{FlushLockfileError, LocalPackage, LocalPackageId};
use crate::lua_version::{LuaVersion, LuaVersionUnset};
use crate::tree::{InstallTree, TreeError};
use crate::{config::Config, tree::Tree};
use bon::Builder;
use futures::StreamExt;
use itertools::Itertools;
use miette::Diagnostic;
use thiserror::Error;
use tracing::{span, Instrument};
#[derive(Error, Debug, Diagnostic)]
#[error(transparent)]
pub enum RemoveError {
    #[diagnostic(transparent)]
    LuaVersionUnset(#[from] LuaVersionUnset),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Fs(#[from] fs::FsError),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Tree(#[from] TreeError),
    #[error(transparent)]
    #[diagnostic(transparent)]
    FlushLockfile(#[from] FlushLockfileError),
}

#[derive(Builder)]
#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
pub struct Uninstall<'a> {
    #[builder(field)]
    packages: Vec<LocalPackageId>,
    config: &'a Config,
    tree: Option<Tree>,
}

impl<'a, State> UninstallBuilder<'a, State>
where
    State: uninstall_builder::State,
{
    /// Add packages to remove.
    pub fn packages<I>(self, packages: I) -> Self
    where
        I: IntoIterator<Item = LocalPackageId>,
    {
        Self {
            packages: self.packages.into_iter().chain(packages).collect_vec(),
            ..self
        }
    }

    /// Add a package to the set of packages to remove.
    pub fn package(self, package: LocalPackageId) -> Self {
        self.packages(std::iter::once(package))
    }
}

impl<'a, State> UninstallBuilder<'a, State>
where
    State: uninstall_builder::State + uninstall_builder::IsComplete,
{
    /// Remove the packages.
    pub async fn remove(self) -> Result<(), RemoveError> {
        let args = self._build();
        let tree = args.tree.unwrap_or(
            args.config
                .user_tree(LuaVersion::from(args.config)?.clone())?,
        );
        remove(args.packages, tree, args.config).await
    }
}

// TODO: Remove dependencies recursively too!
async fn remove(
    package_ids: Vec<LocalPackageId>,
    tree: Tree,
    config: &Config,
) -> Result<(), RemoveError> {
    let lockfile = tree.lockfile()?;

    let packages = package_ids
        .iter()
        .filter_map(|id| lockfile.get(id))
        .cloned()
        .collect_vec();

    futures::stream::iter(packages.into_iter().map(|package| {
        let tree = tree.clone();
        tokio::spawn(
            remove_package(package, tree).instrument(tracing::trace_span!("remove_worker")),
        )
    }))
    .buffered(config.max_jobs())
    .collect::<Vec<_>>()
    .await;

    lockfile.map_then_flush(|lockfile| {
        package_ids
            .iter()
            .for_each(|package| lockfile.remove_by_id(package));

        Ok::<_, io::Error>(())
    })?;

    Ok(())
}

async fn remove_package(package: LocalPackage, tree: Tree) -> Result<(), RemoveError> {
    let span = span!(
        tracing::Level::INFO,
        "Removing",
        package = package.name().to_string(),
        version = package.version().to_string(),
    );
    let _enter = span.enter();

    let rock_layout = tree.installed_rock_layout(&package)?;
    fs::tokio::remove_dir_all(&rock_layout.etc).await?;
    fs::tokio::remove_dir_all(&rock_layout.rock_path).await?;

    for relative_binary_path in package.spec.binaries() {
        if let Some(binary_file_name) = relative_binary_path.file_name() {
            let binary_path = tree.bin().join(binary_file_name);
            if binary_path.is_file() {
                fs::tokio::remove_file(binary_path).await?;
            }

            let unwrapped_binary_path = tree.unwrapped_bin().join(binary_file_name);
            if unwrapped_binary_path.is_file() {
                fs::tokio::remove_file(unwrapped_binary_path).await?;
            }
        }
    }
    Ok(())
}
