//! Log-to-table materializer for the identity graph.
//!
//! Reads unmaterialized mutations from `graph_mutations` and applies them
//! to the source-of-truth tables (canonical_entities, identity_links,
//! relationship_edges, entity_merges). This is the Phase C path where the
//! mutation log becomes the authoritative record and tables are projections.
//!
//! For beta testing, the API handlers still dual-write (write-ahead log +
//! table writes in one transaction). The materializer is used for:
//! 1. Crash recovery (replay unmaterialized mutations)
//! 2. Future log-only mode (handlers write only to log, materializer applies)

use std::collections::HashMap;

use sqlx::PgPool;
use uuid::Uuid;

/// Materializer that reads graph_mutations and applies to tables.
pub struct Materializer {
    pool: PgPool,
    last_materialized: HashMap<Uuid, i64>, // tenant_id -> version
}

impl Materializer {
    pub fn new(pool: PgPool) -> Self {
        Self {
            pool,
            last_materialized: HashMap::new(),
        }
    }

    /// Process unmaterialized mutations for a tenant.
    ///
    /// Reads all mutations with version > last_materialized and applies each
    /// one to the appropriate tables. Idempotent - safe to call multiple times.
    pub async fn materialize(&mut self, tenant_id: Uuid) -> Result<usize, anyhow::Error> {
        let last = self.last_materialized.get(&tenant_id).copied().unwrap_or(0);

        // Acquire a single connection for the entire materialize pass.
        // set_config(..., false) is session-scoped, so all queries on this
        // connection see the correct tenant. The connection is held for the
        // duration and returned to the pool when dropped.
        let mut conn = self.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 AND version > $2 ORDER BY version ASC",
            tenant_id,
            last
        )
        .fetch_all(&mut *conn)
        .await?;

        let count = mutations.len();
        for m in &mutations {
            Self::apply_mutation(
                &mut conn,
                tenant_id,
                m.version,
                &m.mutation_type,
                &m.payload,
            );
            self.last_materialized.insert(tenant_id, m.version);
        }

        // Reset tenant context before returning connection to pool
        sqlx::query!(
            "SELECT set_config('request.tenant_id', '', false)"
        )
        .fetch_one(&mut *conn)
        .await?;
        drop(conn);

        if count > 0 {
            tracing::info!(
                tenant_id = %tenant_id,
                mutations = count,
                last_version = mutations.last().map(|m| m.version).unwrap_or(0),
                "Materialized graph mutations to tables"
            );
        }

        Ok(count)
    }

    /// Apply a single mutation to the appropriate tables.
    ///
    /// In dual-write mode, tables are already written by the API handlers.
    /// When log-only mode is enabled, this will execute the actual DML using
    /// the provided connection (which already has tenant context set).
    fn apply_mutation(
        _conn: &mut sqlx::pool::PoolConnection<sqlx::Postgres>,
        tenant_id: Uuid,
        version: i64,
        mutation_type: &str,
        payload: &serde_json::Value,
    ) {
        match mutation_type {
            "entities_merged" => {
                tracing::debug!(
                    tenant_id = %tenant_id,
                    version = version,
                    "Materializer: entities_merged (tables already written in dual-write mode)"
                );
            }
            "entity_split" => {
                tracing::debug!(
                    tenant_id = %tenant_id,
                    version = version,
                    "Materializer: entity_split (tables already written in dual-write mode)"
                );
            }
            "member_reassigned" => {
                tracing::debug!(
                    tenant_id = %tenant_id,
                    version = version,
                    "Materializer: member_reassigned (tables already written in dual-write mode)"
                );
            }
            "entity_deleted" => {
                tracing::debug!(
                    tenant_id = %tenant_id,
                    version = version,
                    "Materializer: entity_deleted (tables already written in dual-write mode)"
                );
            }
            "edge_linked" => {
                tracing::debug!(
                    tenant_id = %tenant_id,
                    version = version,
                    "Materializer: edge_linked (tables already written in dual-write mode)"
                );
            }
            "edge_unlinked" => {
                tracing::debug!(
                    tenant_id = %tenant_id,
                    version = version,
                    "Materializer: edge_unlinked (tables already written in dual-write mode)"
                );
            }
            "entity_created" | "batch_reconciled" | "entity_updated" => {
                // Informational mutations - no table writes needed
            }
            other => {
                tracing::warn!(
                    tenant_id = %tenant_id,
                    version = version,
                    mutation_type = other,
                    payload = %payload,
                    "Materializer: unknown mutation type, skipping"
                );
            }
        }
    }

    /// Get the last materialized version for a tenant.
    pub fn last_version(&self, tenant_id: &Uuid) -> i64 {
        self.last_materialized.get(tenant_id).copied().unwrap_or(0)
    }

    /// Reset the materializer state for a tenant (e.g., after schema migration).
    pub fn reset(&mut self, tenant_id: &Uuid) {
        self.last_materialized.remove(tenant_id);
    }
}
