//! `SurrealDB` storage backend — `engine::any` dispatcher
//!
//! A single `Surreal<Any>` type unifies all engine variants selected at
//! runtime via URL scheme. The dual hot/cold layout uses:
//! - `graph_db`: `SurrealKV` (default) — B-tree, suited for random-access graph data
//! - `hot_db`: `RocksDB` (default) — LSM, suited for embeddings and session logs
//!
//! ## `SurrealDB` 3.0 and `SurrealValue`
//!
//! `SurrealDB` 3.0 requires `SurrealValue` on all types passed to or returned
//! from CRUD methods. `Graph` and `Node` intentionally do not implement this
//! trait to avoid coupling the domain model to the database library.
//!
//! All I/O routes through `serde_json::Value`, which implements `SurrealValue`,
//! with explicit `serde_json::to_value` / `serde_json::from_value` conversion.
//! This is the same pattern used by `stratum-state`'s `SurrealStateTracker`.

use async_trait::async_trait;
use surrealdb::{engine::any, engine::any::Any, Surreal};

use crate::config::schema::SurrealDbBackendConfig;
use crate::error::{KbError, Result};
use crate::models::{Graph, Node};
use crate::storage::Storage;

// ─── serialization helpers ────────────────────────────────────────────────────

fn to_json<T: serde::Serialize>(v: &T) -> Result<serde_json::Value> {
    serde_json::to_value(v).map_err(|e| KbError::Serialization(e.to_string()))
}

fn from_json<T: serde::de::DeserializeOwned>(v: serde_json::Value) -> Result<T> {
    serde_json::from_value(v).map_err(|e| KbError::Serialization(e.to_string()))
}

// ─── engine factory ───────────────────────────────────────────────────────────

/// Open a `SurrealDB` connection using `engine::any` URL dispatch.
///
/// Calls `use_ns` / `use_db` at connection time so callers never need to
/// repeat the namespace/database selection before individual queries.
async fn open_engine(url: &str, namespace: &str, database: &str) -> Result<Surreal<Any>> {
    let conn = any::connect(url)
        .await
        .map_err(|e| KbError::Storage(format!("connect '{url}' failed: {e}")))?;
    conn.use_ns(namespace)
        .use_db(database)
        .await
        .map_err(|e| KbError::Storage(format!("use_ns/use_db failed: {e}")))?;
    Ok(conn)
}

// ─── storage type ─────────────────────────────────────────────────────────────

/// `SurrealDB` storage backend — no generics, no Arc<`RwLock`<>>
///
/// `Surreal<Any>` is already `Clone + Send + Sync`. The dual-connection
/// layout separates graph semantics from hot-data semantics at the engine
/// level rather than at the table level.
pub struct SurrealDbStorage {
    /// `SurrealKV` (default) or any config-selected engine — graph/node data
    graph_db: Surreal<Any>,
    /// `RocksDB` (default) or any config-selected engine — embeddings/sessions
    hot_db: Surreal<Any>,
    namespace: String,
}

impl SurrealDbStorage {
    /// Construct from a `SurrealDbBackendConfig`, opening both engine connections.
    ///
    /// # Errors
    ///
    /// Returns `KbError::Storage` if either engine connection or namespace/database
    /// selection fails.
    pub async fn from_config(cfg: &SurrealDbBackendConfig) -> Result<Self> {
        let graph_url = cfg.graph.to_url();
        let hot_url = cfg.hot.to_url();
        let graph_db = open_engine(&graph_url, &cfg.namespace, "graph").await?;
        let hot_db = open_engine(&hot_url, &cfg.namespace, "hot").await?;
        Ok(Self {
            graph_db,
            hot_db,
            namespace: cfg.namespace.clone(),
        })
    }

    /// Save a node embedding vector to the hot engine.
    ///
    /// # Errors
    ///
    /// Returns `KbError::Storage` on `SurrealDB` I/O failure.
    pub async fn save_embedding(&self, node_id: &str, vector: &[f32]) -> Result<()> {
        let payload = serde_json::json!({ "node_id": node_id, "vector": vector });
        let _: Option<serde_json::Value> = self
            .hot_db
            .upsert(("embeddings", node_id))
            .content(payload)
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;
        Ok(())
    }

    /// Append a session log entry to the hot engine.
    ///
    /// # Errors
    ///
    /// Returns `KbError::Storage` on `SurrealDB` I/O failure.
    pub async fn log_session(&self, entry: &serde_json::Value) -> Result<()> {
        let _: Option<serde_json::Value> = self
            .hot_db
            .create("sessions")
            .content(entry.clone())
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;
        Ok(())
    }

    fn node_key(graph_name: &str, node_id: &str) -> String {
        format!("{graph_name}__{node_id}")
    }
}

impl Clone for SurrealDbStorage {
    fn clone(&self) -> Self {
        Self {
            graph_db: self.graph_db.clone(),
            hot_db: self.hot_db.clone(),
            namespace: self.namespace.clone(),
        }
    }
}

// ─── Storage trait impl ───────────────────────────────────────────────────────

#[async_trait]
impl Storage for SurrealDbStorage {
    async fn save_graph(&mut self, graph: &Graph) -> Result<()> {
        let _: Option<serde_json::Value> = self
            .graph_db
            .upsert(("graphs", graph.name.clone()))
            .content(to_json(graph)?)
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;

        for node in graph.nodes.values() {
            let key = Self::node_key(&graph.name, &node.id);
            let _: Option<serde_json::Value> = self
                .graph_db
                .upsert(("nodes", key))
                .content(to_json(node)?)
                .await
                .map_err(|e| KbError::Storage(e.to_string()))?;
        }

        Ok(())
    }

    async fn load_graph(&self, name: &str) -> Result<Graph> {
        let raw: Option<serde_json::Value> = self
            .graph_db
            .select(("graphs", name))
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;

        let mut graph: Graph = raw
            .map(from_json)
            .transpose()?
            .ok_or_else(|| KbError::Graph(format!("Graph not found: {name}")))?;

        // Overlay with individually-saved nodes so save_node writes are visible
        let raw_nodes: Vec<serde_json::Value> = self
            .graph_db
            .query("SELECT * FROM nodes WHERE project = $g")
            .bind(("g", name.to_string()))
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?
            .take(0)
            .map_err(|e| KbError::Storage(e.to_string()))?;

        for raw_node in raw_nodes {
            let node: Node = from_json(raw_node)?;
            graph.nodes.insert(node.id.clone(), node);
        }

        Ok(graph)
    }

    async fn save_node(&mut self, node: &Node) -> Result<()> {
        let graph_name = node.project.as_deref().unwrap_or("default");
        let key = Self::node_key(graph_name, &node.id);
        let _: Option<serde_json::Value> = self
            .graph_db
            .upsert(("nodes", key))
            .content(to_json(node)?)
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;
        Ok(())
    }

    async fn load_node(&self, graph_name: &str, node_id: &str) -> Result<Node> {
        let key = Self::node_key(graph_name, node_id);
        let raw: Option<serde_json::Value> = self
            .graph_db
            .select(("nodes", key))
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;

        raw.map(from_json)
            .transpose()?
            .ok_or_else(|| KbError::NodeNotFound(format!("{graph_name}/{node_id}")))
    }

    async fn delete_node(&mut self, graph_name: &str, node_id: &str) -> Result<()> {
        let key = Self::node_key(graph_name, node_id);
        let deleted: Option<serde_json::Value> = self
            .graph_db
            .delete(("nodes", key))
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;

        deleted
            .ok_or_else(|| KbError::NodeNotFound(format!("{graph_name}/{node_id}")))
            .map(|_| ())
    }

    async fn list_graphs(&self) -> Result<Vec<String>> {
        let raw_graphs: Vec<serde_json::Value> = self
            .graph_db
            .select("graphs")
            .await
            .map_err(|e| KbError::Storage(e.to_string()))?;

        raw_graphs
            .into_iter()
            .map(|v| {
                v.get("name")
                    .and_then(|n| n.as_str())
                    .map(str::to_string)
                    .ok_or_else(|| KbError::Storage("graph record missing 'name' field".into()))
            })
            .collect()
    }

    async fn list_nodes(&self, graph_name: &str, node_type: Option<&str>) -> Result<Vec<Node>> {
        let raw_nodes: Vec<serde_json::Value> = if let Some(t) = node_type {
            self.graph_db
                .query("SELECT * FROM nodes WHERE project = $g AND node_type = $t")
                .bind(("g", graph_name.to_string()))
                .bind(("t", t.to_string()))
                .await
                .map_err(|e| KbError::Storage(e.to_string()))?
                .take(0)
                .map_err(|e| KbError::Storage(e.to_string()))?
        } else {
            self.graph_db
                .query("SELECT * FROM nodes WHERE project = $g")
                .bind(("g", graph_name.to_string()))
                .await
                .map_err(|e| KbError::Storage(e.to_string()))?
                .take(0)
                .map_err(|e| KbError::Storage(e.to_string()))?
        };

        raw_nodes.into_iter().map(from_json).collect()
    }
}
