use std::collections::{BTreeMap, HashMap};

use bollard::image::ListImagesOptions;
use color_eyre::eyre::eyre;
use patch_db::{DbHandle, LockType, PatchDbHandle};
use sqlx::{Executor, Sqlite};
use tracing::instrument;

use super::{PKG_ARCHIVE_DIR, PKG_DOCKER_DIR};
use crate::context::RpcContext;
use crate::db::model::{CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry};
use crate::dependencies::reconfigure_dependents_with_live_pointers;
use crate::error::ErrorCollection;
use crate::s9pk::manifest::{Manifest, PackageId};
use crate::util::{Apply, Version};
use crate::Error;

#[instrument(skip(ctx, db, deps))]
pub async fn update_dependency_errors_of_dependents<
    'a,
    Db: DbHandle,
    I: IntoIterator<Item = &'a PackageId>,
>(
    ctx: &RpcContext,
    db: &mut Db,
    id: &PackageId,
    deps: I,
) -> Result<(), Error> {
    for dep in deps {
        if let Some(man) = &*crate::db::DatabaseModel::new()
            .package_data()
            .idx_model(&dep)
            .and_then(|m| m.installed())
            .map::<_, Manifest>(|m| m.manifest())
            .get(db, true)
            .await?
        {
            if let Err(e) = if let Some(info) = man.dependencies.0.get(id) {
                info.satisfied(ctx, db, id, None, dep).await?
            } else {
                Ok(())
            } {
                let mut errs = crate::db::DatabaseModel::new()
                    .package_data()
                    .idx_model(&dep)
                    .expect(db)
                    .await?
                    .installed()
                    .expect(db)
                    .await?
                    .status()
                    .dependency_errors()
                    .get_mut(db)
                    .await?;
                errs.0.insert(id.clone(), e);
                errs.save(db).await?;
            } else {
                let mut errs = crate::db::DatabaseModel::new()
                    .package_data()
                    .idx_model(&dep)
                    .expect(db)
                    .await?
                    .installed()
                    .expect(db)
                    .await?
                    .status()
                    .dependency_errors()
                    .get_mut(db)
                    .await?;
                errs.0.remove(id);
                errs.save(db).await?;
            }
        }
    }
    Ok(())
}

#[instrument(skip(ctx))]
pub async fn cleanup(ctx: &RpcContext, id: &PackageId, version: &Version) -> Result<(), Error> {
    let mut errors = ErrorCollection::new();
    ctx.managers.remove(&(id.clone(), version.clone())).await;
    // docker images start9/$APP_ID/*:$VERSION -q | xargs docker rmi
    let images = ctx
        .docker
        .list_images(Some(ListImagesOptions {
            all: false,
            filters: {
                let mut f = HashMap::new();
                f.insert(
                    "reference".to_owned(),
                    vec![format!("start9/{}/*:{}", id, version)],
                );
                f
            },
            digests: false,
        }))
        .await
        .apply(|res| errors.handle(res));
    errors.extend(
        futures::future::join_all(images.into_iter().flatten().map(|image| async {
            let image = image; // move into future
            ctx.docker.remove_image(&image.id, None, None).await
        }))
        .await,
    );
    let pkg_archive_dir = ctx
        .datadir
        .join(PKG_ARCHIVE_DIR)
        .join(id)
        .join(version.as_str());
    if tokio::fs::metadata(&pkg_archive_dir).await.is_ok() {
        tokio::fs::remove_dir_all(&pkg_archive_dir)
            .await
            .apply(|res| errors.handle(res));
    }
    let docker_path = ctx
        .datadir
        .join(PKG_DOCKER_DIR)
        .join(id)
        .join(version.as_str());
    if tokio::fs::metadata(&docker_path).await.is_ok() {
        tokio::fs::remove_dir_all(&docker_path)
            .await
            .apply(|res| errors.handle(res));
    }

    errors.into_result()
}

#[instrument(skip(ctx, db))]
pub async fn cleanup_failed<Db: DbHandle>(
    ctx: &RpcContext,
    db: &mut Db,
    id: &PackageId,
) -> Result<(), Error> {
    crate::db::DatabaseModel::new()
        .package_data()
        .lock(db, LockType::Write)
        .await?;
    let pde = crate::db::DatabaseModel::new()
        .package_data()
        .idx_model(id)
        .expect(db)
        .await?
        .get(db, true)
        .await?
        .into_owned();
    if let Some(manifest) = match &pde {
        PackageDataEntry::Installing { manifest, .. }
        | PackageDataEntry::Restoring { manifest, .. } => Some(manifest),
        PackageDataEntry::Updating {
            manifest,
            installed:
                InstalledPackageDataEntry {
                    manifest: installed_manifest,
                    ..
                },
            ..
        } => {
            if &manifest.version != &installed_manifest.version {
                Some(manifest)
            } else {
                None
            }
        }
        _ => {
            tracing::warn!("{}: Nothing to clean up!", id);
            None
        }
    } {
        cleanup(ctx, id, &manifest.version).await?;
    }

    match pde {
        PackageDataEntry::Installing { .. } | PackageDataEntry::Restoring { .. } => {
            crate::db::DatabaseModel::new()
                .package_data()
                .remove(db, id)
                .await?;
        }
        PackageDataEntry::Updating {
            installed,
            static_files,
            ..
        } => {
            crate::db::DatabaseModel::new()
                .package_data()
                .idx_model(id)
                .put(
                    db,
                    &PackageDataEntry::Installed {
                        manifest: installed.manifest.clone(),
                        installed,
                        static_files,
                    },
                )
                .await?;
        }
        _ => (),
    }

    Ok(())
}

#[instrument(skip(db, current_dependencies))]
pub async fn remove_from_current_dependents_lists<
    'a,
    Db: DbHandle,
    I: IntoIterator<Item = &'a PackageId>,
>(
    db: &mut Db,
    id: &'a PackageId,
    current_dependencies: I,
) -> Result<(), Error> {
    for dep in current_dependencies.into_iter().chain(std::iter::once(id)) {
        if let Some(current_dependents) = crate::db::DatabaseModel::new()
            .package_data()
            .idx_model(dep)
            .and_then(|m| m.installed())
            .map::<_, BTreeMap<PackageId, CurrentDependencyInfo>>(|m| m.current_dependents())
            .check(db)
            .await?
        {
            if current_dependents
                .clone()
                .idx_model(id)
                .exists(db, true)
                .await?
            {
                current_dependents.remove(db, id).await?
            }
        }
    }
    Ok(())
}

#[instrument(skip(ctx, secrets, db))]
pub async fn uninstall<Ex>(
    ctx: &RpcContext,
    db: &mut PatchDbHandle,
    secrets: &mut Ex,
    id: &PackageId,
) -> Result<(), Error>
where
    for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
{
    let mut tx = db.begin().await?;
    crate::db::DatabaseModel::new()
        .package_data()
        .lock(&mut tx, LockType::Write)
        .await?;
    let entry = crate::db::DatabaseModel::new()
        .package_data()
        .idx_model(id)
        .and_then(|pde| pde.removing())
        .get(&mut tx, true)
        .await?
        .into_owned()
        .ok_or_else(|| {
            Error::new(
                eyre!("Package not in removing state: {}", id),
                crate::ErrorKind::NotFound,
            )
        })?;
    cleanup(ctx, &entry.manifest.id, &entry.manifest.version).await?;

    crate::db::DatabaseModel::new()
        .package_data()
        .remove(&mut tx, id)
        .await?;

    // once we have removed the package entry, we can change all the dependent pointers to null
    reconfigure_dependents_with_live_pointers(ctx, &mut tx, &entry).await?;

    remove_from_current_dependents_lists(
        &mut tx,
        &entry.manifest.id,
        entry.current_dependencies.keys(),
    )
    .await?;
    update_dependency_errors_of_dependents(
        ctx,
        &mut tx,
        &entry.manifest.id,
        entry.current_dependents.keys(),
    )
    .await?;
    let volumes = ctx
        .datadir
        .join(crate::volume::PKG_VOLUME_DIR)
        .join(&entry.manifest.id);
    if tokio::fs::metadata(&volumes).await.is_ok() {
        tokio::fs::remove_dir_all(&volumes).await?;
    }
    tx.commit(None).await?;
    remove_tor_keys(secrets, &entry.manifest.id).await?;
    Ok(())
}

#[instrument(skip(secrets))]
pub async fn remove_tor_keys<Ex>(secrets: &mut Ex, id: &PackageId) -> Result<(), Error>
where
    for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
{
    let id_str = id.as_str();
    sqlx::query!("DELETE FROM tor WHERE package = ?", id_str)
        .execute(secrets)
        .await?;
    Ok(())
}
