use std::error::Error;

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

use crate::{
    config::{self, AppConfig},
    credentials::Session,
    ipc,
    server::Server,
    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| {
            app.get_window("main")
                .map(|w| w.show().error_popup("Failed to show main window"));
        }))
        .system_tray(tray::create())
        .on_system_tray_event(tray::handle_event)
        .invoke_handler(tauri::generate_handler![
            ipc::unlock,
            ipc::respond,
            ipc::get_session_status,
            ipc::save_credentials,
            ipc::get_config,
            ipc::save_config,
            ipc::launch_terminal,
            ipc::get_setup_errors,
        ])
        .setup(|app| rt::block_on(setup(app)))
        .build(tauri::generate_context!())?
        .run(|app, run_event| match run_event {
            tauri::RunEvent::WindowEvent { label, event, .. } => match event {
                tauri::WindowEvent::CloseRequested { api, .. } => {
                    let _ = app.get_window(&label).map(|w| w.hide());
                    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()).unwrap();

    // 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 conf = match AppConfig::load(&pool).await {
        Ok(c) => c,
        Err(SetupError::ConfigParseError(_)) => {
            setup_errors.push(
                "Could not load configuration from database. Reverting to defaults.".into()
            );
            AppConfig::default()
        },
        err => err?,
    };

    let session = Session::load(&pool).await?;
    Server::start(app.handle())?;

    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 let Err(e) = shortcuts::register_hotkeys(&conf.hotkeys) {
        setup_errors.push(format!("{e}"));
    }

    // if session is empty, this is probably the first launch, so don't autohide
    if !conf.start_minimized || is_first_launch {
        app.get_window("main")
            .ok_or(HandlerError::NoMainWindow)?
            .show()?;
    }

    let state = AppState::new(conf, session, pool, setup_errors);
    app.manage(state);
    Ok(())
}
