use std::future::Future;

use borg::task::Task;
use common::borg;
use common::borg::{RepoId, task};
use enclose::enclose;
use gio::prelude::*;

use crate::error::Combined;
use crate::prelude::*;
use crate::{App, utils};

/// Is a borg operation registered with a [QuitGuard]]?
pub fn is_borg_operation_running() -> bool {
    STATUS_TRACKING.with(|status| status.quit_inhibit_count() > 0)
}

/// Checks whether borg command line arguments are valid
pub fn parse_borg_command_line_args(text: &str) -> Result<Vec<String>> {
    if let Ok(args) = shell_words::split(text) {
        Ok(args)
    } else {
        Err(Message::new(
            gettext("Additional command line arguments invalid"),
            gettext("Please check for missing closing quotes."),
        )
        .into())
    }
}

/// The actual execution of a borg task
async fn do_exec<T: Task>(mut command: borg::Command<T>) -> CombinedResult<T::Return>
where
    borg::Command<T>: borg::CommandRun<T>,
{
    let mounted_result = crate::repo::ensure_repo_available(
        &App::default().main_window(),
        &command.config,
        &T::name(),
    )
    .await;

    match mounted_result {
        Ok(config) => command.config = config,
        Err(err) => {
            // The repository is not available after trying to mount it
            return Err(
                borg::Error::Aborted(borg::Abort::RepositoryNotAvailable(err.to_string())).into(),
            );
        }
    }

    spawn_borg_thread_ask_password(command).await
}

/// Executes a borg command
///
/// This takes a [QuitGuard] to prove that one has been set up and is currently
/// active.
pub async fn exec<T: Task>(
    command: borg::Command<T>,
    _guard: &QuitGuard,
) -> CombinedResult<T::Return>
where
    borg::Command<T>: borg::CommandRun<T>,
{
    let config_id = command.config.id.clone();

    if T::KIND != borg::task::Kind::Mount {
        // If a repository is mounted we ask to unmount it before we continue
        ask_unmount(T::KIND, &command.config.repo_id).await?;
    }

    // Setup: Create a borg operation, write running state to history

    BORG_OPERATION.with(enclose!((command) move |operations| {
        if let Some(operation) = operations
            .load()
            .values()
            .find(|x| x.repo_id() == &command.config.repo_id)
        {
            return Err(Combined::Ui(
                Message::new(gettext("Repository already in use"), operation.name()).into(),
            ));
        }

        crate::operation::Operation::register(command);

        Ok(())
    }))?;

    BACKUP_HISTORY
        .try_update(enclose!((config_id) move |history| {
            history.set_running(config_id.clone());
            Ok(())
        }))
        .await?;

    // Execute the borg command, and store the result
    let result = do_exec(command).await;

    // Cleanup: Remove operation, remove running state from history

    BORG_OPERATION.with(enclose!((config_id) move |operations| {
        operations.update(|op| {
            op.remove(&config_id);
        });
    }));

    // Handle an error when writing history, but still return the result of the borg
    // operation
    Handler::handle(
        BACKUP_HISTORY
            .try_update(move |history| {
                history.remove_running(config_id.clone());
                Ok(())
            })
            .await,
    );

    result
}

pub async fn exec_repo_only<P: core::fmt::Display, F, R, V>(
    name: P,
    borg: borg::CommandOnlyRepo,
    task: F,
) -> CombinedResult<V>
where
    F: FnOnce(borg::CommandOnlyRepo) -> R + Send + Clone + 'static + Sync,
    R: Future<Output = borg::Result<V>>,
    V: Send + 'static,
{
    spawn_borg_thread(name, borg, task).await
}

async fn ask_unmount(kind: task::Kind, repo_id: &RepoId) -> Result<()> {
    crate::utils::borg::cleanup_repo_mounts().await;

    if BACKUP_HISTORY
        .load()
        .browsing_repo_ids(&BACKUP_CONFIG.load())
        .contains(repo_id)
    {
        tracing::debug!("Trying to run a {kind:?} on a backup that is currently mounted.");

        match kind {
            task::Kind::Create => {
                crate::utils::confirmation_dialog(
                    &App::default().main_window(),
                    &gettext("Stop Browsing Files?"),
                    &gettext(
                        "Browsing through archived files is not possible while running a backup",
                    ),
                    &gettext("Keep Browsing"),
                    &gettext("Start Backup"),
                )
                .await?;
            }
            _ => {
                crate::utils::confirmation_dialog(
                    &App::default().main_window(),
                    &gettext("Stop Browsing Files?"),
                    &gettext(
                        "Browsing through archived files is not possible while running an operation on the repository",
                    ),
                    &gettext("Keep Browsing"),
                    &gettext("Start Operation"),
                )
                .await?;
            }
        }

        tracing::trace!("User decided to unmount repo.");
        repo_unmount(repo_id)
            .await
            .err_to_msg(gettext("Failed to unmount repository."))?;
    }

    Ok(())
}

async fn spawn_borg_thread_ask_password<C: 'static + borg::CommandRun<T>, T: Task>(
    mut command: C,
) -> CombinedResult<T::Return> {
    let mut password_changed = false;

    loop {
        let result = spawn_borg_thread(T::name(), command.clone(), |x| x.run()).await;

        return match result {
            Err(Combined::Borg(borg::Error::PasswordMissing { .. }))
            | Err(Combined::Borg(borg::Error::Failed(borg::log_json::LogMessage {
                msgid: borg::Failure::PassphraseWrong,
                ..
            }))) => {
                let keyring_error =
                    if let Err(Combined::Borg(borg::Error::PasswordMissing { keyring_error })) =
                        result
                    {
                        keyring_error
                    } else {
                        None
                    };

                match crate::utils::password_storage::password_dialog(
                    &command.repo(),
                    &T::name(),
                    keyring_error.as_deref(),
                )
                .await
                {
                    Some(password) => {
                        command.set_password(password);
                        password_changed = true;

                        continue;
                    }
                    _ => Err(Error::UserCanceled.into()),
                }
            }
            _ => {
                if password_changed
                    && let (Some(password), Some(config)) =
                        (&command.password(), &command.try_config())
                {
                    if let Err(Error::Message(err)) =
                        crate::utils::password_storage::store_password(config, password).await
                    {
                        tracing::warn!(
                            "Error using keyring, using in-memory password store. Keyring error: '{err:?}'"
                        );

                        // Use the in-memory password store instead
                        common::globals::MEMORY_PASSWORD_STORE
                            .set_password(config, password.clone());
                    }

                    if !config.encrypted {
                        // We assumed that the repo doesn't have encryption, but this assumption was
                        // outdated. Set the encrypted flag in the config
                        if let Some(id) = command.config_id() {
                            BACKUP_CONFIG
                                .try_update(|config| {
                                    let cfg = config.try_get_mut(&id)?;
                                    cfg.encrypted = true;

                                    Ok(())
                                })
                                .await?;
                        }
                    }
                }
                result
            }
        };
    }
}

async fn spawn_borg_thread<P, F, R, V, B>(name: P, borg: B, task: F) -> CombinedResult<V>
where
    P: core::fmt::Display,
    F: FnOnce(B) -> R + Send + Clone + 'static + Sync,
    R: Future<Output = borg::Result<V>>,
    V: Send + 'static,
    B: borg::BorgRunConfig,
{
    loop {
        let result = super::spawn_thread(
            name.to_string(),
            enclose!((borg, task) move || {
               smol::block_on(task(borg))
            }),
        )
        .await;

        return match result? {
            Err(borg::Error::Failed(borg::log_json::LogMessage {
                msgid: borg::Failure::LockTimeout,
                ..
            })) if !borg.is_scheduled() => {
                // Ask to break lock for manually started backups
                handle_lock(borg.clone()).await?;
                continue;
            }
            Err(e) => Err(e.into()),
            Ok(result) => Ok(result),
        };
    }
}

async fn handle_lock<B: borg::BorgRunConfig>(borg: B) -> CombinedResult<()> {
    let repo_location = borg.repo().location();
    crate::utils::ConfirmationDialog::new(
        &gettext("Repository Already in Use"),
        &(gettextf("The backup repository “{}” is marked as already in use. This information can be outdated if, for example, the computer lost power while using the repository.", [repo_location])
        + "\n\n"
        + &gettext("Only continue if it is certain that the repository is not used by any program! Continuing while another program uses the repository might corrupt backup data!")),
        &gettext("Cancel"),
        &gettext("Continue Anyway"),
    )
    .set_destructive(true)
    .ask(&App::default().main_window())
    .await?;

    super::spawn_thread("borg_break_lock", move || {
        borg::CommandOnlyRepo::new(borg.repo()).break_lock()
    })
    .await
    .map_err(|_| borg::Error::ThreadPanicked)?
    .await
    .map_err(Into::into)
}

pub async fn repo_unmount(repo_id: &RepoId) -> Result<()> {
    borg::functions::umount(repo_id)
        .await
        .err_to_msg(gettext("Failed to unmount repository."))?;
    utils::borg::unset_repo_browsing(repo_id).await;

    let configs = BACKUP_CONFIG.load();
    let backup_ids = configs
        .iter()
        .filter(|x| x.repo_id == *repo_id)
        .map(|x| &x.id)
        .collect::<Vec<_>>();

    BACKUP_HISTORY
        .try_update(|histories| {
            for id in backup_ids.iter() {
                histories.remove_browsing((*id).clone());
            }
            Ok(())
        })
        .await?;

    main_ui().page_detail().archives_page().refresh_status();

    Ok(())
}

pub async fn set_repo_browsing(repo_id: &RepoId) {
    let configs = BACKUP_CONFIG.load();
    let backup_ids = configs
        .iter()
        .filter(|x| x.repo_id == *repo_id)
        .map(|x| &x.id)
        .collect::<Vec<_>>();

    let _ = BACKUP_HISTORY
        .try_update(|histories| {
            for id in backup_ids.iter() {
                histories.set_browsing((*id).clone());
            }
            Ok(())
        })
        .await;
}

pub async fn unset_repo_browsing(repo_id: &RepoId) {
    let configs = BACKUP_CONFIG.load();
    let backup_ids = configs
        .iter()
        .filter(|x| x.repo_id == *repo_id)
        .map(|x| &x.id)
        .collect::<Vec<_>>();

    let _ = BACKUP_HISTORY
        .try_update(|histories| {
            for id in backup_ids.iter() {
                histories.remove_browsing((*id).clone());
            }
            Ok(())
        })
        .await;
}

pub async fn cleanup_repo_mounts() {
    let mounts = BACKUP_HISTORY
        .load()
        .browsing_repo_ids(&BACKUP_CONFIG.load());

    // Find mounts that were already unmounted outside of Pika
    for repo_id in mounts.iter() {
        if !borg::is_mounted(repo_id).await {
            // The repository was unmounted somewhere else
            // Call unmount to fix the state
            tracing::warn!(
                "Marking repo {repo_id:?} as unmounted, as the mountpoint doesn't exist anymore"
            );
            if let Err(err) = repo_unmount(repo_id).await {
                tracing::warn!("Failed to run the borg unmount procedure for '{repo_id:?}': {err}");
            }
        }
    }

    // Find mounts that should belong to Pika but aren't registered.
    // This would be leftover mounts from a previous run of Pika when it wasn't quit
    // properly.
    for config in BACKUP_CONFIG.load().iter() {
        let repo_id = &config.repo_id;
        if !mounts.contains(repo_id) && borg::is_mounted(repo_id).await {
            tracing::warn!(
                "Marking repo {repo_id:?} as mounted, was probably mounted from a force-quit app"
            );
            utils::borg::set_repo_browsing(repo_id).await;
        }
    }
}

pub async fn unmount_backup_disk(backup: common::config::Backup) -> Result<()> {
    match backup.repo.removable_drive_volume() {
        Some(volume) => {
            // We have a removable drive and found a volume
            let mount_operation = gtk::MountOperation::new(Some(&main_ui().window()));

            match volume
                .drive()
                .and_then(|drive| if drive.can_eject() { Some(drive) } else { None })
            {
                Some(drive) => {
                    // We don't need to stop the drive, it will only spin down the hard disk. The
                    // drive is safe to remove in any case
                    let res = if drive.can_stop() {
                        tracing::debug!("Stopping drive {}", drive.name());
                        drive
                            .stop_future(gio::MountUnmountFlags::empty(), Some(&mount_operation))
                            .await
                    } else {
                        tracing::debug!("Ejecting drive {}", drive.name());
                        drive
                            .eject_with_operation_future(
                                gio::MountUnmountFlags::empty(),
                                Some(&mount_operation),
                            )
                            .await
                    };

                    if let Err(err) = res {
                        if let Some(gio::IOErrorEnum::FailedHandled) = err.kind() {
                            tracing::debug!("Unmount aborted by user: {}", err);
                            return Ok(());
                        } else {
                            tracing::debug!("Error ejecting disk: {}", err);
                            return Err(Message::new(
                                gettext("Unable to Eject Backup Disk"),
                                err.to_string(),
                            )
                            .into());
                        }
                    }

                    // When the drive was ejected we can show a toast
                    let toast = adw::Toast::builder()
                        .title(gettextf("{} can be safely unplugged.", [&drive.name()]))
                        .timeout(5)
                        .build();

                    main_ui().toast().add_toast(toast);
                }
                _ => {
                    tracing::debug!(
                        "Unmount disk: Backup disk {} can't be ejected",
                        volume.name()
                    );
                    Err(Message::new(
                        gettext("Unable to Eject Backup Disk"),
                        gettextf("{} can't be ejected.", [&volume.name()]),
                    ))?;
                }
            }
        }
        _ => {
            tracing::debug!("Unmount disk: Backup disk not found");
        }
    }

    Ok(())
}

#[gtk::test]
async fn test_exec_operation_register() {
    let _app = crate::App::new();

    let mut config = common::config::Backup::test_new_mock();
    config.schedule.frequency = common::config::Frequency::Hourly;

    let command = borg::Command::<borg::task::List>::new(config)
        .set_from_schedule(Some(common::schedule::DueCause::Regular));

    assert!(!is_borg_operation_running());

    {
        let guard = QuitGuard::default();
        assert!(is_borg_operation_running());

        assert!(exec(command.clone(), &guard).await.is_err());
        assert!(is_borg_operation_running());
    }

    assert!(!is_borg_operation_running());
}
