//! Wrappers around the Tauri `invoke` API.
//!
//! Each function maps directly to a Tauri command defined in `src-tauri`.
//! On non-wasm32 targets (native unit test runs) every function returns an
//! error immediately — they are never called in that context.

use std::collections::HashMap;

use serde::Serialize;

use brittle_model::{
    Annotation, AnnotationId, AnnotationType, Library, LibraryId, Reference, ReferenceId,
    ReferenceSummary,
};

// ── Low-level invoke ───────────────────────────────────────────────────────────

/// Empty argument struct for commands that take no user parameters.
#[derive(Serialize)]
struct NoArgs {}

#[cfg(target_arch = "wasm32")]
mod ffi {
    use wasm_bindgen::prelude::*;

    #[wasm_bindgen]
    extern "C" {
        /// `window.__TAURI__.core.invoke(cmd, args)`
        ///
        /// `catch` turns a JS exception (e.g. `window.__TAURI__` undefined when
        /// running in a plain browser) into `Err(JsValue)` instead of a WASM
        /// trap, so callers can degrade gracefully.
        #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke, catch)]
        pub fn invoke_js(cmd: &str, args: JsValue) -> Result<js_sys::Promise, JsValue>;
    }
}

/// Invoke a Tauri command, serialise `args` as JSON and deserialise the result.
async fn invoke<T: for<'de> serde::Deserialize<'de>, A: Serialize>(
    cmd: &str,
    args: &A,
) -> Result<T, String> {
    #[cfg(target_arch = "wasm32")]
    {
        use wasm_bindgen_futures::JsFuture;
        let args_js =
            serde_wasm_bindgen::to_value(args).map_err(|e| e.to_string())?;
        let promise = ffi::invoke_js(cmd, args_js)
            .map_err(|_| "Tauri IPC unavailable (not running inside the desktop app)".to_string())?;
        let result = JsFuture::from(promise)
            .await
            .map_err(|e| format!("{e:?}"))?;
        serde_wasm_bindgen::from_value::<T>(result).map_err(|e| e.to_string())
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        let _ = (cmd, args);
        Err("tauri not available outside of wasm32".into())
    }
}

#[derive(serde::Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub struct LayoutConfig {
    pub left_pane_fraction: f32,
    pub right_pane_fraction: f32,
    pub left_pane_min: i32,
    pub left_pane_max: i32,
    pub right_pane_min: i32,
    pub right_pane_max: i32,
    pub center_pane_min: i32,
}

// ── Session types ─────────────────────────────────────────────────────────────

#[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct PdfTabState {
    pub zoom: f64,
    pub scroll_top: f64,
}

#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct PdfTabEntry {
    pub ref_id: String,
    pub title: String,
}

#[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
pub struct SessionConfig {
    pub open_tabs: Vec<PdfTabEntry>,
    pub active_tab: usize,
    pub pdf_states: std::collections::HashMap<String, PdfTabState>,
}

// ── Config commands ────────────────────────────────────────────────────────────

/// Return the session state for the currently open project.
pub async fn get_session() -> Result<SessionConfig, String> {
    invoke("get_session", &NoArgs {}).await
}

/// Persist the session state for the currently open project.
pub async fn save_session(session: &SessionConfig) -> Result<(), String> {
    #[derive(Serialize)]
    struct Args<'a> { session: &'a SessionConfig }
    invoke("save_session", &Args { session }).await
}

/// Return the last-opened project path from the global config, or `None` if
/// none was recorded or the directory no longer exists on disk.
pub async fn get_last_project() -> Result<Option<String>, String> {
    invoke("get_last_project", &NoArgs {}).await
}

/// Return the user's layout configuration from the global config.
pub async fn get_layout_config() -> Result<LayoutConfig, String> {
    invoke("get_layout_config", &NoArgs {}).await
}

/// Return the user's keybinding overrides from `~/.config/brittle/config.toml`.
///
/// Keys are snake_case action names (e.g. `"tab_next"`); values are
/// key-sequence strings (e.g. `"<C-Right>"`).
pub async fn get_keybindings() -> Result<HashMap<String, String>, String> {
    invoke("get_keybindings", &NoArgs {}).await
}

// ── Appearance commands ────────────────────────────────────────────────────────

/// Return the current theme name (`"dark"` or `"light"`) from the global config.
pub async fn get_theme() -> Result<String, String> {
    invoke("get_theme", &NoArgs {}).await
}

/// Persist a new theme choice (`"dark"` or `"light"`) to the global config.
pub async fn set_theme(theme: &str) -> Result<(), String> {
    #[derive(Serialize)]
    struct Args<'a> {
        theme: &'a str,
    }
    invoke("set_theme", &Args { theme }).await
}

// ── Repository commands ────────────────────────────────────────────────────────

pub async fn open_repository(path: &str) -> Result<(), String> {
    #[derive(Serialize)]
    struct Args<'a> {
        path: &'a str,
    }
    invoke("open_repository", &Args { path }).await
}

// ── Library commands ───────────────────────────────────────────────────────────

pub async fn list_root_libraries() -> Result<Vec<Library>, String> {
    invoke("list_root_libraries", &NoArgs {}).await
}

pub async fn list_child_libraries(parent_id: &LibraryId) -> Result<Vec<Library>, String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        parent_id: &'a LibraryId,
    }
    invoke("list_child_libraries", &Args { parent_id }).await
}

// ── Library membership commands ────────────────────────────────────────────────

/// Add a reference to a library (additive; does not move the reference).
pub async fn add_to_library(
    library_id: &LibraryId,
    reference_id: &ReferenceId,
) -> Result<(), String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        library_id: &'a LibraryId,
        reference_id: &'a ReferenceId,
    }
    invoke("add_to_library", &Args { library_id, reference_id }).await
}

// ── Reference commands ─────────────────────────────────────────────────────────

pub async fn list_references() -> Result<Vec<ReferenceSummary>, String> {
    invoke("list_references", &NoArgs {}).await
}

pub async fn list_library_references(
    library_id: &LibraryId,
) -> Result<Vec<ReferenceSummary>, String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        library_id: &'a LibraryId,
    }
    invoke("list_library_references", &Args { library_id }).await
}

pub async fn list_library_references_recursive(
    library_id: &LibraryId,
) -> Result<Vec<ReferenceSummary>, String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        library_id: &'a LibraryId,
    }
    invoke("list_library_references_recursive", &Args { library_id }).await
}

pub async fn search_references(query: &str) -> Result<Vec<ReferenceSummary>, String> {
    #[derive(Serialize)]
    struct Args<'a> {
        query: &'a str,
    }
    invoke("search_references", &Args { query }).await
}

pub async fn search_library_references(
    library_id: &LibraryId,
    query: &str,
) -> Result<Vec<ReferenceSummary>, String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        library_id: &'a LibraryId,
        query: &'a str,
    }
    invoke("search_library_references", &Args { library_id, query }).await
}

pub async fn get_reference(id: &ReferenceId) -> Result<Reference, String> {
    #[derive(Serialize)]
    struct Args<'a> {
        id: &'a ReferenceId,
    }
    invoke("get_reference", &Args { id }).await
}

// ── Annotation commands ────────────────────────────────────────────────────────

pub async fn get_annotations(reference_id: &ReferenceId) -> Result<Vec<Annotation>, String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        reference_id: &'a ReferenceId,
    }
    invoke("get_annotations", &Args { reference_id }).await
}

pub async fn create_annotation(
    reference_id: &ReferenceId,
    page: u32,
    annotation_type: AnnotationType,
    content: Option<String>,
) -> Result<Annotation, String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        reference_id: &'a ReferenceId,
        page: u32,
        annotation_type: AnnotationType,
        content: Option<String>,
    }
    invoke("create_annotation", &Args { reference_id, page, annotation_type, content }).await
}

pub async fn update_annotation(annotation: Annotation) -> Result<Annotation, String> {
    #[derive(Serialize)]
    struct Args {
        annotation: Annotation,
    }
    invoke("update_annotation", &Args { annotation }).await
}

pub async fn delete_annotation(
    reference_id: &ReferenceId,
    annotation_id: &AnnotationId,
) -> Result<(), String> {
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    struct Args<'a> {
        reference_id: &'a ReferenceId,
        annotation_id: &'a AnnotationId,
    }
    invoke("delete_annotation", &Args { reference_id, annotation_id }).await
}
