use rusqlite::Connection;
use rusqlite::types::Value as SqlValue;
use serde_json::Value as JsonValue;

// ponytail: a read-only SQL viewer for the two SQLite DBs (lore.db,
// generations.db). We gate on the statement starting with SELECT / PRAGMA /
// WITH so a curious DM can inspect the backend but can't mutate it from the
// UI. Enough for "what's going on in the backend" without a footgun.

/// Run a read-only query against `conn`. Returns (column names, rows).
pub fn run_readonly_query(
    conn: &Connection,
    sql: &str,
) -> anyhow::Result<(Vec<String>, Vec<Vec<JsonValue>>)> {
    let trimmed = sql.trim().trim_end_matches(';').trim();
    if trimmed.is_empty() {
        return Ok((Vec::new(), Vec::new()));
    }
    let upper = trimmed.to_uppercase();
    let allowed = upper.starts_with("SELECT")
        || upper.starts_with("PRAGMA")
        || upper.starts_with("WITH");
    if !allowed {
        anyhow::bail!("Only read-only queries (SELECT / PRAGMA / WITH) are allowed");
    }

    let mut stmt = conn.prepare(trimmed)?;
    let col_count = stmt.column_count();
    let columns: Vec<String> = (0..col_count)
        .map(|i| stmt.column_name(i).unwrap_or("?").to_string())
        .collect();
    let rows = stmt.query_map([], |row| {
        let mut vals = Vec::with_capacity(col_count);
        for i in 0..col_count {
            let v: SqlValue = row.get(i)?;
            vals.push(sql_to_json(v));
        }
        Ok(vals)
    })?;
    let mut out = Vec::new();
    for r in rows {
        out.push(r?);
    }
    Ok((columns, out))
}

fn sql_to_json(v: SqlValue) -> JsonValue {
    match v {
        SqlValue::Null => JsonValue::Null,
        SqlValue::Integer(i) => i.into(),
        SqlValue::Real(f) => f.into(),
        SqlValue::Text(s) => JsonValue::String(s),
        // ponytail: blobs aren't useful to render raw — show a size hint so the
        // DM knows there's data without dumping megabytes of embedding bytes.
        SqlValue::Blob(b) => JsonValue::String(format!("<blob {} bytes>", b.len())),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn mem() -> Connection {
        let c = Connection::open_in_memory().unwrap();
        c.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, weight REAL);").unwrap();
        c.execute("INSERT INTO t (name, weight) VALUES (?1, ?2), (?1, ?2)", rusqlite::params!["goblin", 23.5]).unwrap();
        c
    }

    #[test]
    fn select_returns_columns_and_rows() {
        let (cols, rows) = run_readonly_query(&mem(), "SELECT id, name, weight FROM t ORDER BY id").unwrap();
        assert_eq!(cols, vec!["id".to_string(), "name".into(), "weight".into()]);
        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0][1], serde_json::json!("goblin"));
        assert_eq!(rows[0][2], serde_json::json!(23.5));
    }

    #[test]
    fn rejects_writes() {
        let c = mem();
        let err = run_readonly_query(&c, "DELETE FROM t").unwrap_err();
        assert!(err.to_string().contains("read-only"));
        let err = run_readonly_query(&c, "INSERT INTO t (name, weight) VALUES ('x', 1)").unwrap_err();
        assert!(err.to_string().contains("read-only"));
        let err = run_readonly_query(&c, "UPDATE t SET name='x'").unwrap_err();
        assert!(err.to_string().contains("read-only"));
    }

    #[test]
    fn pragma_table_info_allowed() {
        let (cols, rows) = run_readonly_query(&mem(), "PRAGMA table_info(t)").unwrap();
        assert!(cols.contains(&"name".to_string()));
        assert!(rows.iter().any(|r| r.contains(&serde_json::json!("id"))));
    }

    #[test]
    fn with_cte_allowed() {
        let (_cols, rows) = run_readonly_query(&mem(), "WITH x AS (SELECT 1 AS n) SELECT * FROM x").unwrap();
        assert_eq!(rows.len(), 1);
    }

    #[test]
    fn empty_and_trailing_semicolon_ok() {
        let (cols, rows) = run_readonly_query(&mem(), "SELECT 1 AS n;").unwrap();
        assert_eq!(cols, vec!["n"]);
        assert_eq!(rows.len(), 1);
        let Ok((c, r)) = run_readonly_query(&mem(), "   ") else { panic!("empty query failed") };
        assert!(c.is_empty() && r.is_empty());
    }
}