//! Graph registry - manages resident in-memory identity graphs per tenant.
//!
//! Replaces the moka TTL-based cache with a persistent registry. The graph IS
//! the truth - nothing to invalidate. LRU eviction when memory pressure exceeds
//! the max_resident threshold.
//!
//! On first access for a tenant, the graph is built from DB tables
//! (canonical_entities, relationship_edges, entity_merges, identity_links).
//! After that, mutations keep the graph in sync (Phase A: dual-write).

#[cfg(feature = "server")]
mod inner {
    use std::collections::HashMap;
    use std::sync::Arc;
    use std::time::Instant;

    use chrono::Utc;
    use sqlx::PgPool;
    use tokio::sync::RwLock;
    use uuid::Uuid;

    use crate::graph::model::{
        EdgeKind, GraphEdge, IdentityGraph, NodeData,
    };

    /// Manages resident in-memory identity graphs for active tenants.
    pub struct GraphRegistry {
        graphs: RwLock<HashMap<Uuid, Arc<RwLock<IdentityGraph>>>>,
        last_accessed: RwLock<HashMap<Uuid, Instant>>,
        max_resident: usize,
    }

    impl GraphRegistry {
        pub fn new(max_resident: usize) -> Self {
            Self {
                graphs: RwLock::new(HashMap::new()),
                last_accessed: RwLock::new(HashMap::new()),
                max_resident,
            }
        }

        /// Get or load the identity graph for a tenant.
        ///
        /// Returns `Arc<RwLock<IdentityGraph>>` - callers acquire read/write
        /// locks as needed for queries or mutations.
        pub async fn get_or_load(
            &self,
            tenant_id: Uuid,
            pool: &PgPool,
        ) -> Result<Arc<RwLock<IdentityGraph>>, anyhow::Error> {
            // Fast path: check if already loaded
            {
                let graphs = self.graphs.read().await;
                if let Some(graph) = graphs.get(&tenant_id) {
                    let mut accessed = self.last_accessed.write().await;
                    accessed.insert(tenant_id, Instant::now());
                    return Ok(graph.clone());
                }
            }

            // Slow path: build from DB
            let ig = Self::build_from_db(pool, tenant_id).await?;
            let arc = Arc::new(RwLock::new(ig));

            // Store in registry
            {
                let mut graphs = self.graphs.write().await;
                let mut accessed = self.last_accessed.write().await;

                // Double-check after acquiring write lock
                if let Some(existing) = graphs.get(&tenant_id) {
                    accessed.insert(tenant_id, Instant::now());
                    return Ok(existing.clone());
                }

                // Evict LRU if at capacity
                if graphs.len() >= self.max_resident {
                    Self::evict_lru_inner(&mut graphs, &mut accessed);
                }

                graphs.insert(tenant_id, arc.clone());
                accessed.insert(tenant_id, Instant::now());
            }

            Ok(arc)
        }

        /// Invalidate and remove a tenant's graph from the registry.
        ///
        /// The graph will be rebuilt from DB on next access.
        pub async fn invalidate(&self, tenant_id: &Uuid) {
            let mut graphs = self.graphs.write().await;
            let mut accessed = self.last_accessed.write().await;
            graphs.remove(tenant_id);
            accessed.remove(tenant_id);
        }

        /// Invalidate all cached graphs matching a version key pattern.
        ///
        /// Compatibility method for callers that previously used
        /// `graph_cache.invalidate(&(tenant_id, version))`. Simply delegates
        /// to `invalidate` since the registry doesn't use version keys.
        pub async fn invalidate_by_key(&self, tenant_id: &Uuid) {
            self.invalidate(tenant_id).await;
        }

        /// Build an IdentityGraph from DB tables for a given tenant.
        ///
        /// Loads canonical_entities, identity_links (for member counts),
        /// relationship_edges, and entity_merges. Syncs version from the
        /// graph_mutations log. Computes analytics eagerly.
        async fn build_from_db(
            pool: &PgPool,
            tenant_id: Uuid,
        ) -> Result<IdentityGraph, anyhow::Error> {
            let mut conn = pool.acquire().await?;

            // Set tenant context for RLS
            let tid = tenant_id.to_string();
            sqlx::query!(
                "SELECT set_config('request.tenant_id', $1::text, false)",
                tid
            )
            .fetch_one(&mut *conn)
            .await?;

            let mut ig = IdentityGraph::new(tenant_id);

            // 1. Load canonical entities with member counts
            let entity_rows = sqlx::query!(
                r#"SELECT ce.id, ce.entity_type,
                        COALESCE(ce.is_locked, false) as "is_locked!: bool",
                        ce.created_at, ce.updated_at,
                        ce.canonical_data,
                        COUNT(il.id) as "member_count!: i64"
                 FROM canonical_entities ce
                 LEFT JOIN identity_links il
                     ON il.canonical_entity_id = ce.id
                     AND il.tenant_id = ce.tenant_id
                 WHERE ce.tenant_id = $1
                 GROUP BY ce.id
                 LIMIT 50000"#,
                tenant_id
            )
            .fetch_all(&mut *conn)
            .await?;

            for row in &entity_rows {
                let canonical_data = &row.canonical_data;

                // Extract display_name from canonical_data
                let display_name = canonical_data
                    .get("full_name")
                    .or_else(|| canonical_data.get("name"))
                    .or_else(|| canonical_data.get("display_name"))
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string());

                ig.add_node(NodeData {
                    id: row.id,
                    entity_type: row.entity_type.clone(),
                    member_count: row.member_count as u32,
                    display_name,
                    is_locked: row.is_locked,
                    created_at: row.created_at,
                    updated_at: row.updated_at,
                });
            }

            // 2. Load relationship edges (soft edges)
            let rel_rows = sqlx::query!(
                "SELECT canonical_a_id, canonical_b_id, weight, attributes \
                 FROM relationship_edges \
                 WHERE tenant_id = $1 \
                 ORDER BY weight DESC \
                 LIMIT 50000",
                tenant_id
            )
            .fetch_all(&mut *conn)
            .await?;

            // Merge edges between same canonical pair (take max weight)
            let mut merged: HashMap<(Uuid, Uuid), (f64, serde_json::Value)> =
                HashMap::new();
            for row in &rel_rows {
                let a = row.canonical_a_id;
                let b = row.canonical_b_id;
                let weight = row.weight;
                let attrs = row.attributes.clone();

                let key = if a < b { (a, b) } else { (b, a) };
                let entry = merged
                    .entry(key)
                    .or_insert((0.0, serde_json::json!([])));
                if weight > entry.0 {
                    *entry = (weight, attrs);
                }
            }

            let now = Utc::now();
            for ((a, b), (weight, attrs)) in &merged {
                // Ensure both nodes exist (they may not if they were only in
                // relationship_edges but not in canonical_entities)
                if !ig.node_index.contains_key(a) {
                    ig.add_node(NodeData {
                        id: *a,
                        entity_type: "unknown".into(),
                        member_count: 0,
                        display_name: None,
                        is_locked: false,
                        created_at: now,
                        updated_at: now,
                    });
                }
                if !ig.node_index.contains_key(b) {
                    ig.add_node(NodeData {
                        id: *b,
                        entity_type: "unknown".into(),
                        member_count: 0,
                        display_name: None,
                        is_locked: false,
                        created_at: now,
                        updated_at: now,
                    });
                }

                ig.add_edge(
                    *a,
                    *b,
                    GraphEdge {
                        kind: EdgeKind::Relationship,
                        weight: *weight,
                        attributes: Some(attrs.clone()),
                        created_at: now,
                    },
                );
            }

            // 3. Load entity merges as identity edges
            let merge_rows = sqlx::query!(
                "SELECT DISTINCT winner_entity_id, loser_entity_id \
                 FROM entity_merges \
                 WHERE tenant_id = $1 \
                 LIMIT 50000",
                tenant_id
            )
            .fetch_all(&mut *conn)
            .await?;

            for row in &merge_rows {
                let winner = row.winner_entity_id;
                let loser = row.loser_entity_id;

                // Ensure both nodes exist
                for nid in [winner, loser] {
                    if !ig.node_index.contains_key(&nid) {
                        ig.add_node(NodeData {
                            id: nid,
                            entity_type: "unknown".into(),
                            member_count: 0,
                            display_name: None,
                            is_locked: false,
                            created_at: now,
                            updated_at: now,
                        });
                    }
                }

                // Only add if no edge already exists
                let w_idx = ig.node_index[&winner];
                let l_idx = ig.node_index[&loser];
                if ig.graph.find_edge(w_idx, l_idx).is_none() {
                    ig.add_edge(
                        winner,
                        loser,
                        GraphEdge {
                            kind: EdgeKind::Identity,
                            weight: 1.0,
                            attributes: None,
                            created_at: now,
                        },
                    );
                }
            }

            // 4. Sync version from graph_mutations log
            let max_version: Option<i64> = sqlx::query_scalar!(
                "SELECT MAX(version) FROM graph_mutations WHERE tenant_id = $1",
                tenant_id
            )
            .fetch_one(&mut *conn)
            .await?;
            ig.version = max_version.unwrap_or(0) as u64;

            // 5. Compute analytics eagerly
            ig.recompute_analytics();

            if ig.graph.node_count() > 50_000 {
                tracing::warn!(
                    tenant_id = %tenant_id,
                    nodes = ig.graph.node_count(),
                    "Tenant graph exceeds 50k node cap"
                );
            }

            Ok(ig)
        }

        /// Build an IdentityGraph by replaying the graph_mutations log (Phase C path).
        ///
        /// Instead of reading from canonical_entities/identity_links/relationship_edges
        /// tables, this reconstructs the graph purely from the mutation log. Used for
        /// crash recovery and as the future source-of-truth path.
        #[allow(dead_code)]
        pub async fn build_from_log(
            pool: &PgPool,
            tenant_id: Uuid,
        ) -> Result<IdentityGraph, anyhow::Error> {
            let mut conn = pool.acquire().await?;

            let tid = tenant_id.to_string();
            sqlx::query!(
                "SELECT set_config('request.tenant_id', $1::text, false)",
                tid
            )
            .fetch_one(&mut *conn)
            .await?;

            let mutations = sqlx::query!(
                "SELECT version, mutation_type, payload \
                 FROM graph_mutations \
                 WHERE tenant_id = $1 \
                 ORDER BY version ASC",
                tenant_id
            )
            .fetch_all(&mut *conn)
            .await?;

            let mut ig = IdentityGraph::new(tenant_id);
            for m in &mutations {
                if let Err(e) = ig.apply_mutation_from_log(
                    m.version,
                    &m.mutation_type,
                    &m.payload,
                ) {
                    tracing::warn!(
                        tenant_id = %tenant_id,
                        version = m.version,
                        error = %e,
                        "Skipping unplayable mutation during log replay"
                    );
                }
            }

            ig.recompute_analytics();
            Ok(ig)
        }

        /// Evict the least recently used tenant graph.
        fn evict_lru_inner(
            graphs: &mut HashMap<Uuid, Arc<RwLock<IdentityGraph>>>,
            accessed: &mut HashMap<Uuid, Instant>,
        ) {
            if let Some((&oldest_id, _)) =
                accessed.iter().min_by_key(|(_, instant)| **instant)
            {
                tracing::info!(
                    tenant_id = %oldest_id,
                    "Evicting LRU tenant graph from registry"
                );
                graphs.remove(&oldest_id);
                accessed.remove(&oldest_id);
            }
        }
    }
}

#[cfg(feature = "server")]
pub use inner::GraphRegistry;
