use std::path::PathBuf;

use auto_launch::AutoLaunchBuilder;
use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize};
use sqlx::SqlitePool;
use tauri::{
    Manager,
    GlobalShortcutManager,
    async_runtime as rt,
};

use crate::errors::*;


#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TermConfig {
    pub name: String,
    // we call it exec because it isn't always the actual path,
    // in some cases it's just the name and relies on path-searching
    // it's a string because it can come from the frontend as json
    pub exec: String,
    pub args: Vec<String>,
}


#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Hotkey {
    pub keys: String,
    pub enabled: bool,
}


#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct HotkeysConfig {
    // tauri uses strings to represent keybinds, so we will as well
    pub show_window: Hotkey,
    pub launch_terminal: Hotkey,
}


#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppConfig {
    #[serde(default = "default_rehide_ms")]
    pub rehide_ms: u64,
    #[serde(default = "default_start_minimized")]
    pub start_minimized: bool,
    #[serde(default = "default_start_on_login")]
    pub start_on_login: bool,
    #[serde(default = "default_term_config")]
    pub terminal: TermConfig,
    #[serde(default = "default_hotkey_config")]
    pub hotkeys: HotkeysConfig,
}


impl Default for AppConfig {
    fn default() -> Self {
        AppConfig {
            rehide_ms: default_rehide_ms(),
            start_minimized: default_start_minimized(),
            start_on_login: default_start_on_login(),
            terminal: default_term_config(),
            hotkeys: default_hotkey_config(),
        }
    }
}


impl AppConfig {
    pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
        let res = sqlx::query!("SELECT * from config where name = 'main'")
            .fetch_optional(pool)
            .await?;

        let row = match res {
            Some(row) => row,
            None => return Ok(AppConfig::default()),
        };

        Ok(serde_json::from_str(&row.data)?)
    }

    pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
        let data = serde_json::to_string(self).unwrap();
        sqlx::query(
            "INSERT INTO config (name, data) VALUES ('main', ?)
            ON CONFLICT (name) DO UPDATE SET data = ?"
        )
            .bind(&data)
            .bind(&data)
            .execute(pool)
            .await?;

        Ok(())
    }
}


pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> {
    let path_buf = std::env::current_exe()
        .map_err(|e| auto_launch::Error::Io(e))?;
    let path = path_buf
        .to_string_lossy();

    let auto = AutoLaunchBuilder::new()
        .set_app_name("Creddy")
        .set_app_path(&path)
        .build()?;

    let is_enabled = auto.is_enabled()?;
    if is_configured && !is_enabled {
        auto.enable()?;
    }
    else if !is_configured && is_enabled {
        auto.disable()?;
    }

    Ok(())
}


pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
    let mut path = dirs::data_dir()
        .ok_or(DataDirError::NotFound)?;
    path.push("Creddy");

    std::fs::create_dir_all(&path)?;
    if cfg!(debug_assertions) && std::io::stdout().is_terminal() {
        path.push("creddy.dev.db");
    }
    else {
        path.push("creddy.db");
    }

    Ok(path)
}


fn default_term_config() -> TermConfig {
    #[cfg(windows)]
    {
        let shell = if which::which("pwsh.exe").is_ok() {
            "pwsh.exe".to_string()
        }
        else {
            "powershell.exe".to_string()
        };

        let (exec, args) = if cfg!(debug_assertions) {
            ("conhost.exe".to_string(), vec![shell.clone()])
        } else {
            (shell.clone(), vec![])
        };

        TermConfig { name: shell, exec, args }
    }

    #[cfg(unix)]
    {
        for bin in ["gnome-terminal", "konsole"] {
            if let Ok(_) = which::which(bin) {
                return TermConfig {
                    name: bin.into(),
                    exec: bin.into(),
                    args: vec![],
                }
            }
        }
        return TermConfig {
            name: "gnome-terminal".into(),
            exec: "gnome-terminal".into(),
            args: vec![],
        };
    }
}


fn default_hotkey_config() -> HotkeysConfig {
    HotkeysConfig {
        show_window: Hotkey {keys: "alt+shift+C".into(), enabled: true},
        launch_terminal: Hotkey {keys: "alt+shift+T".into(), enabled: true},
    }
}

// note: will panic if called before APP is set
pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
    let app = crate::app::APP.get().unwrap();

    let mut manager = app.global_shortcut_manager();
    manager.unregister_all()?;

    if hotkeys.show_window.enabled {
        let handle = app.app_handle();
        manager.register(
            &hotkeys.show_window.keys,
            move || { 
                handle.get_window("main")
                    .map(|w| w.show().error_popup("Failed to show"))
                    .ok_or(HandlerError::NoMainWindow)
                    .error_popup("No main window");
            },
        )?;
    }

    if hotkeys.launch_terminal.enabled {
        // register() doesn't take an async fn, so we have to use spawn
        manager.register(
            &hotkeys.launch_terminal.keys,
            || { 
                rt::spawn(async {
                    crate::terminal::launch(false)
                        .await
                        .error_popup("Failed to launch");
                });
            }
        )?;
    }

    Ok(())
}


fn default_rehide_ms() -> u64 { 1000 }
// start minimized and on login only in production mode
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
fn default_start_on_login() -> bool { !cfg!(debug_assertions) }
