//! `qubes-sqlite` — a SQLite engine (built on Turso) for the qubepods agent
//! harness, packaged as a Continuum **library qube**.
//!
//! Two layers:
//!   * [`engine`] — the portable, host-agnostic SQLite wrapper over
//!     `turso_core`. Plain Rust, tested natively.
//!   * the browser surface below (`#[cfg(target_arch = "wasm32")]`) — a
//!     `wasm_bindgen` class that drives the engine from JavaScript inside a Web
//!     Worker. This is the form `apps/app` loads in the qubepods shell.
//!
//! The WIT contract in `wit/sqlite.wit` is the qube's canonical Continuum
//! identity; the wasm-bindgen surface is the browser runtime form of the same
//! engine until component transpile (jco) / `qube wac` linking land.

pub mod engine;

pub use engine::{Engine, EngineError, QueryResult, RunResult, SqlValue};

// ---------------------------------------------------------------------------
// Browser (Web Worker) surface.
// ---------------------------------------------------------------------------
#[cfg(target_arch = "wasm32")]
mod browser {
    use crate::engine::{Engine, SqlValue};
    use wasm_bindgen::prelude::*;

    /// An open in-memory SQLite database, callable from JS.
    ///
    /// ```js
    /// import init, { Sqlite } from "./pkg/qubes_sqlite.js";
    /// await init();
    /// const db = new Sqlite();
    /// db.execBatch("CREATE TABLE t(a INTEGER, b TEXT);");
    /// db.run("INSERT INTO t VALUES (?, ?)", [1, "hi"]);
    /// const { columns, rows } = db.query("SELECT * FROM t WHERE a = ?", [1]);
    /// ```
    #[wasm_bindgen]
    pub struct Sqlite {
        engine: Engine,
    }

    #[wasm_bindgen]
    impl Sqlite {
        /// Open a fresh in-memory database.
        #[wasm_bindgen(constructor)]
        pub fn new() -> Result<Sqlite, JsError> {
            console_error_panic_hook::set_once();
            let engine = Engine::open_in_memory().map_err(to_js)?;
            Ok(Sqlite { engine })
        }

        /// Execute one or more `;`-separated statements with no parameters and
        /// no result rows (DDL / batch setup).
        #[wasm_bindgen(js_name = execBatch)]
        pub fn exec_batch(&self, sql: &str) -> Result<(), JsError> {
            self.engine.execute_batch(sql).map_err(to_js)
        }

        /// Run a row-producing statement. `params` is a JS array of values
        /// (`null`, number, bigint, boolean, string, or `Uint8Array`).
        /// Returns `{ columns: string[], rows: any[][] }`.
        #[wasm_bindgen]
        pub fn query(&self, sql: &str, params: JsValue) -> Result<JsValue, JsError> {
            let params = js_array_to_params(params)?;
            let result = self.engine.query(sql, &params).map_err(to_js)?;

            let cols = js_sys::Array::new();
            for c in &result.columns {
                cols.push(&JsValue::from_str(c));
            }
            let rows = js_sys::Array::new();
            for row in &result.rows {
                let r = js_sys::Array::new();
                for v in row {
                    r.push(&sql_value_to_js(v));
                }
                rows.push(&r);
            }
            let out = js_sys::Object::new();
            set(&out, "columns", &cols)?;
            set(&out, "rows", &rows)?;
            Ok(out.into())
        }

        /// Open a database from a serialized SQLite image (e.g. an uploaded
        /// `.sqlite` file's bytes). Static: `Sqlite.fromBytes(uint8array)`.
        #[wasm_bindgen(js_name = fromBytes)]
        pub fn from_bytes(bytes: &[u8]) -> Result<Sqlite, JsError> {
            console_error_panic_hook::set_once();
            let engine = Engine::open_from_bytes(bytes).map_err(to_js)?;
            Ok(Sqlite { engine })
        }

        /// Serialize the whole database to a SQLite file image (a `Uint8Array`
        /// of the `.sqlite` bytes) — for download/save.
        #[wasm_bindgen(js_name = exportDatabase)]
        pub fn export_database(&self) -> Result<Vec<u8>, JsError> {
            self.engine.export().map_err(to_js)
        }

        /// Run a row-less statement (`INSERT`/`UPDATE`/`DELETE`). `params` is a
        /// JS array as in [`query`]. Returns
        /// `{ changes: number, lastInsertRowid: number }`.
        #[wasm_bindgen]
        pub fn run(&self, sql: &str, params: JsValue) -> Result<JsValue, JsError> {
            let params = js_array_to_params(params)?;
            let result = self.engine.run(sql, &params).map_err(to_js)?;
            let out = js_sys::Object::new();
            set(&out, "changes", &JsValue::from_f64(result.changes as f64))?;
            set(
                &out,
                "lastInsertRowid",
                &JsValue::from_f64(result.last_insert_rowid as f64),
            )?;
            Ok(out.into())
        }
    }

    fn to_js(e: crate::engine::EngineError) -> JsError {
        JsError::new(&e.to_string())
    }

    /// Set a string-keyed property on a JS object, mapping the `JsValue` error
    /// `Reflect::set` returns into a `JsError` so `?` composes.
    fn set(obj: &js_sys::Object, key: &str, val: &JsValue) -> Result<(), JsError> {
        js_sys::Reflect::set(obj, &JsValue::from_str(key), val)
            .map(|_| ())
            .map_err(|e| JsError::new(&format!("failed to set '{key}': {e:?}")))
    }

    /// Convert a JS params array into engine `SqlValue`s. Accepts `null`/
    /// `undefined`, number, bigint, boolean, string and `Uint8Array`.
    fn js_array_to_params(params: JsValue) -> Result<Vec<SqlValue>, JsError> {
        if params.is_null() || params.is_undefined() {
            return Ok(Vec::new());
        }
        let arr = params
            .dyn_into::<js_sys::Array>()
            .map_err(|_| JsError::new("params must be an array"))?;
        let mut out = Vec::with_capacity(arr.length() as usize);
        for v in arr.iter() {
            out.push(js_to_sql_value(&v)?);
        }
        Ok(out)
    }

    fn js_to_sql_value(v: &JsValue) -> Result<SqlValue, JsError> {
        if v.is_null() || v.is_undefined() {
            return Ok(SqlValue::Null);
        }
        if let Some(b) = v.as_bool() {
            return Ok(SqlValue::Integer(if b { 1 } else { 0 }));
        }
        if let Some(n) = v.as_f64() {
            // Integral floats bind as INTEGER, the rest as REAL.
            if n.fract() == 0.0 && n.abs() < 9_007_199_254_740_992.0 {
                return Ok(SqlValue::Integer(n as i64));
            }
            return Ok(SqlValue::Real(n));
        }
        if let Some(s) = v.as_string() {
            return Ok(SqlValue::Text(s));
        }
        if let Some(big) = v.dyn_ref::<js_sys::BigInt>() {
            // BigInt → i64 where it fits.
            if let Ok(i) = i64::try_from(big.clone()) {
                return Ok(SqlValue::Integer(i));
            }
            return Err(JsError::new("bigint out of i64 range"));
        }
        if let Some(bytes) = v.dyn_ref::<js_sys::Uint8Array>() {
            return Ok(SqlValue::Blob(bytes.to_vec()));
        }
        Err(JsError::new(
            "unsupported parameter type (use null, number, bigint, boolean, string, or Uint8Array)",
        ))
    }

    fn sql_value_to_js(v: &SqlValue) -> JsValue {
        match v {
            SqlValue::Null => JsValue::NULL,
            // Integers within the safe range come back as JS numbers; larger
            // ones as BigInt so no precision is silently lost.
            SqlValue::Integer(i) => {
                if i.unsigned_abs() < (1u64 << 53) {
                    JsValue::from_f64(*i as f64)
                } else {
                    JsValue::from(*i) // BigInt
                }
            }
            SqlValue::Real(f) => JsValue::from_f64(*f),
            SqlValue::Text(s) => JsValue::from_str(s),
            SqlValue::Blob(b) => js_sys::Uint8Array::from(b.as_slice()).into(),
        }
    }
}
