use std::cell::{Cell, OnceCell};
use std::collections::HashMap;
use std::path::PathBuf;

use common::config::{ConfigType, Loadable, TrackChanges};
use common::utils::action::Action;
use common::{DAEMON_BINARY, config};
use gio::ApplicationHoldGuard;
use gio::prelude::*;

use super::action;
use crate::prelude::*;

pub fn init() {
    gio_app().connect_startup(on_startup);
}

thread_local! {
    static HOLD: OnceCell<ApplicationHoldGuard> = OnceCell::default();
    static FILE_MONITOR_FLATPAK_UPDATED: OnceCell<gio::FileMonitor> = OnceCell::default();
    static APP_RUNNING: Cell<bool> = Cell::default();
}

fn on_startup(_app: &gio::Application) {
    HOLD.with(|hold| hold.set(gio_app().hold()).unwrap());

    tracing::info!("Starting monitor v{}", crate::VERSION);

    common::utils::init_gettext();

    // Load configs and connect file change handlers for reload
    let config_load_result = config::Histories::update_on_change(
        &BACKUP_HISTORY,
        config_reload_error_handler(config::Histories::path()),
    )
    .and_then(|_| {
        config::Backups::update_on_change(
            &BACKUP_CONFIG,
            config_reload_error_handler(config::Backups::path()),
        )
    });

    if let Err(err) = &config_load_result {
        let msg = gettext("Error loading configuration");
        let detail = format!("{}\n{}", gettext("Not monitoring backup schedule."), err);
        tracing::error!("{msg}: {detail}");

        let notification = gio::Notification::new(&msg.to_string());
        notification.set_body(Some(&detail));
        gio_app().send_notification(None, &notification);

        // If we can't read the config, quit the monitor process
        tracing::error!(
            "Exiting monitor v{} due to an unreadable config.",
            crate::VERSION
        );
        gio_app().quit();
        return;
    }

    crate::connect::init::init();
    crate::schedule::init::init();

    gio_app().add_action(&action::Restart::action());
    gio_app().add_action(&action::Quit::action());
    gio_app().add_action(&action::StartBackup::action());
    gio_app().add_action(&action::ShowOverview::action());
    gio_app().add_action(&action::ShowSchedule::action());

    glib::MainContext::default().spawn(async {
        match ashpd::desktop::background::BackgroundProxy::new().await {
            Ok(proxy) => {
                let options = ashpd::desktop::background::SetStatusOptions::default()
                    .set_message(&gettext("Monitoring backup schedule"));
                if let Err(err) = proxy.set_status(options).await {
                    tracing::debug!("Error setting background status: {err:?}");
                }
            }
            Err(err) => tracing::error!("Error acquiring background proxy: {err:?}"),
        }

        common::utils::listen_remote_app_running(
            common::APP_ID,
            &crate::dbus::session_connection()
                .await
                .handle("Cannot monitor ui status.")?,
            app_running,
        )
        .await
        .handle("Cannot monitor ui status.")
    });

    if *APP_IS_SANDBOXED {
        // Register a file monitor to check for the /app/.updated file
        // Creation of this file signifies that the app had been updated
        let file = gio::File::for_path("/app/.updated");

        if let Ok(monitor) = file.monitor_file(gio::FileMonitorFlags::NONE, gio::Cancellable::NONE)
        {
            monitor.connect_changed(|_monitor, _file, _other, event| {
                if event == gio::FileMonitorEvent::Created {
                    if APP_RUNNING.get() {
                        tracing::info!("Detected flatpak update. Not restarting monitor process, main app is still running.");
                    } else {
                        tracing::info!("Detected flatpak update. Restarting monitor process.");
                        glib::MainContext::default().block_on(restart_daemon());
                    }
                }
            });

            FILE_MONITOR_FLATPAK_UPDATED
                .with(|m| m.set(monitor))
                .unwrap();
        }
    }
}

fn app_running(is_running: bool) {
    APP_RUNNING.set(is_running);

    if !is_running {
        // Reload backup history manually to prevent race conditions between the
        // application exit event and file monitor
        match config::Histories::from_file() {
            Ok(new) => {
                BACKUP_HISTORY.swap(Arc::new(new));
            }
            Err(err) => {
                tracing::error!(
                    "Failed to reload {path:?}: '{err}'. (monitor v{version})",
                    path = config::Histories::path(),
                    version = crate::VERSION
                );
            }
        }

        let backups_running = BACKUP_HISTORY
            .load()
            .iter()
            .filter(|(_, x)| x.is_running())
            .count();

        if backups_running > 0 {
            let notification = gio::Notification::new(&gettext("Fatal Error During Back Up"));

            notification.set_body(Some(&ngettextf_(
                "Pika Backup crashed while running a backup",
                "Pika Backup crashed while running {} backups",
                backups_running as u32,
            )));

            notification.set_default_action(&action::ShowOverview::name());

            gio_app().send_notification(None, &notification);
        }

        // Detect app update
        if *APP_IS_SANDBOXED && PathBuf::from("/app/.updated").exists() {
            tracing::info!("Detected flatpak update. Restarting monitor process.");
            glib::MainContext::default().spawn(restart_daemon());
        }
    }
}

pub fn config_reload_error_handler(path: PathBuf) -> impl Fn(std::io::Error) {
    move |err| {
        tracing::warn!(
            "Failt to reload {path:?}: '{err}'. Restarting monitor. (Monitor v{version})",
            version = crate::VERSION
        );
        tracing::info!("If restarting the monitor succeeds, the previous warning can be ignored.");
        glib::MainContext::default().spawn(restart_daemon());
    }
}

pub async fn restart_daemon() {
    if *APP_IS_SANDBOXED {
        let flatpak_result = ashpd::flatpak::Flatpak::new().await;
        if let Ok(flatpak) = flatpak_result {
            let binary = PathBuf::from("/app/bin/").join(DAEMON_BINARY);

            flatpak
                .spawn(
                    glib::current_dir(),
                    &[binary.as_os_str(), "--gapplication-replace".as_ref()],
                    HashMap::<u32, std::os::fd::OwnedFd>::new(),
                    HashMap::new(),
                    ashpd::flatpak::SpawnFlags::LatestVersion.into(),
                    ashpd::flatpak::SpawnOptions::default(),
                )
                .await
                .handle(gettext("Error restarting monitor monitor"));
        } else {
            flatpak_result.handle(gettext("Error restarting monitor monitor"));
        }
    } else {
        let mut command = async_process::Command::new(DAEMON_BINARY);
        command.arg("--gapplication-replace");
        command
            .spawn()
            .handle(gettext("Error restarting monitor monitor"));
    }
}
