use std::error::Error;
use std::time::Duration;

use once_cell::sync::OnceCell;
use sqlx::{
    SqlitePool,
    sqlite::SqlitePoolOptions,
    sqlite::SqliteConnectOptions,
};
use tauri::{
    App,
    AppHandle,
    async_runtime as rt,
    Manager,
    RunEvent,
    WindowEvent,
};
use tauri::menu::MenuItem;

use crate::{
    config::{self, AppConfig},
    credentials::AppSession,
    ipc,
    srv::{creddy_server, agent},
    errors::*,
    shortcuts,
    state::AppState,
    tray,
};


pub static APP: OnceCell<AppHandle> = OnceCell::new();


pub fn run() -> tauri::Result<()> {
    tauri::Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
            show_main_window(app)
                .error_popup("Failed to show main window")
        }))
        .plugin(tauri_plugin_global_shortcut::Builder::default().build())
        .plugin(tauri_plugin_os::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![
            ipc::unlock,
            ipc::lock,
            ipc::reset_session,
            ipc::set_passphrase,
            ipc::respond,
            ipc::get_session_status,
            ipc::signal_activity,
            ipc::save_credential,
            ipc::delete_credential,
            ipc::list_credentials,
            ipc::sshkey_from_file,
            ipc::sshkey_from_private_key,
            ipc::get_config,
            ipc::save_config,
            ipc::launch_terminal,
            ipc::get_setup_errors,
            ipc::exit,
        ])
        .setup(|app| rt::block_on(setup(app)))
        .build(tauri::generate_context!())?
        .run(|app, run_event| {
            if let RunEvent::WindowEvent { event, .. } = run_event {
                if let WindowEvent::CloseRequested { api, .. } = event {
                    let _ = hide_main_window(app);
                    api.prevent_close();
                }
            }
        });

    Ok(())
}


pub async fn connect_db() -> Result<SqlitePool, SetupError> {
    let conn_opts = SqliteConnectOptions::new()
        .filename(config::get_or_create_db_path()?)
        .create_if_missing(true);
    let pool_opts = SqlitePoolOptions::new();
    let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
    sqlx::migrate!().run(&pool).await?;
    Ok(pool)
}


async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
    APP.set(app.handle().clone()).unwrap();
    tray::setup(app)?;
    // get_or_create_db_path doesn't create the actual db file, just the directory
    let is_first_launch = !config::get_or_create_db_path()?.exists();
    let pool = connect_db().await?;
    let mut setup_errors: Vec<String> = vec![];

    let mut conf = match AppConfig::load(&pool).await {
        Ok(c) => c,
        Err(LoadKvError::Invalid(_)) => {
            setup_errors.push(
                "Could not load configuration from database. Reverting to defaults.".into()
            );
            AppConfig::default()
        },
        err => err?,
    };

    let app_session = AppSession::load(&pool).await?;
    creddy_server::serve(app.handle().clone())?;
    agent::serve(app.handle().clone())?;

    config::set_auto_launch(conf.start_on_login)?;
    if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
        setup_errors.push("Error: Failed to manage autolaunch.".into());
    }

    // if hotkeys fail to register, disable them so that this error doesn't have to keep showing up
    if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) {
        conf.hotkeys.disable_all();
        conf.save(&pool).await?;
        setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into());
    }

    let desktop_is_gnome = std::env::var("XDG_CURRENT_DESKTOP")
        .map(|names| names.split(':').any(|n| n == "GNOME"))
        .unwrap_or(false);

    if !conf.start_minimized || is_first_launch {
        show_main_window(&app.handle())?;
    }

    let state = AppState::new(conf, app_session, pool, setup_errors, desktop_is_gnome);
    app.manage(state);

    // make sure we do this after managing app state, so that it doesn't panic
    start_auto_locker(app.app_handle().clone());

    Ok(())
}


fn start_auto_locker(app: AppHandle) {
    rt::spawn(async move {
        let state = app.state::<AppState>();
        loop {
            // this gives our session-timeout a minimum resolution of 10s, which seems fine?
            let delay = Duration::from_secs(10);
            tokio::time::sleep(delay).await;

            if state.should_auto_lock().await {
                state.lock().await.error_popup("Failed to lock Creddy");
            }
        }
    });
}


pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> {
    let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
    w.show()?;
    let show_hide = app.state::<MenuItem<tauri::Wry>>();
    show_hide.set_text("Hide")?;
    Ok(())
}


pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> {
    let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
    w.hide()?;
    let show_hide = app.state::<MenuItem<tauri::Wry>>();
    show_hide.set_text("Show")?;
    Ok(())
}


pub fn toggle_main_window(app: &AppHandle) -> Result<(), WindowError> {
    let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?;
    if w.is_visible()? {
        hide_main_window(app)
    }
    else {
        show_main_window(app)
    }
}
