//! Shared merge operations - single source of truth for entity merges.
//!
//! Every merge in the system (auto_merge agent, approved actions, realtime resolve,
//! CRM sync) MUST go through `execute_merge()`. This ensures consistent behavior:
//! - identity_links are moved from loser to winner
//! - entity_merges record is created (audit trail)
//! - entity_events event is emitted (downstream systems)
//! - canonical_data is optionally recomputed (survivorship)
//! - loser is optionally deleted (realtime path)

use serde::{Deserialize, Serialize};
use sqlx::PgConnection;
use sqlx::Row;
use uuid::Uuid;

/// The type/source of a merge operation.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MergeType {
    /// Batch reconciliation pipeline
    Auto,
    /// Real-time resolve path
    Realtime,
    /// Agent-initiated merge (auto_merge agent)
    Agent,
    /// Manual merge (CRM sync, admin action)
    Manual,
}

impl MergeType {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Auto => "auto",
            Self::Realtime => "realtime",
            Self::Agent => "agent",
            Self::Manual => "manual",
        }
    }
}

impl std::fmt::Display for MergeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Request to merge two canonical entities.
pub struct MergeRequest {
    pub tenant_id: Uuid,
    pub winner_canonical_id: Uuid,
    pub loser_canonical_id: Uuid,
    pub merge_type: MergeType,
    pub confidence: Option<f64>,
    /// Arbitrary metadata included in the entity_events payload.
    pub metadata: serde_json::Value,
}

/// Options controlling merge behavior.
pub struct MergeOptions {
    /// Delete the loser canonical_entity row after merge (realtime path needs this).
    pub delete_loser: bool,
    /// Recompute canonical_data via last-non-null survivorship.
    pub recompute_canonical: bool,
}

impl Default for MergeOptions {
    fn default() -> Self {
        Self {
            delete_loser: false,
            recompute_canonical: true,
        }
    }
}

/// Result of a successful merge.
pub struct MergeResult {
    pub winner_id: Uuid,
    pub loser_id: Uuid,
    /// Number of identity_links moved from loser to winner.
    pub members_moved: i64,
    /// Total members on winner after merge.
    pub total_members: i64,
}

/// Execute a merge of two canonical entities within the caller's transaction.
///
/// The caller owns the transaction boundary. This function takes a `&mut PgConnection`
/// (which can be a transaction or a raw connection) and performs all merge steps
/// atomically within whatever transaction context the caller provides.
///
/// Steps:
/// 1. Verify winner != loser
/// 2. Count loser's members
/// 3. Move identity_links from loser to winner
/// 4. Record entity_merges row
/// 5. Emit entity.merged event
/// 6. Optionally recompute canonical_data (survivorship)
/// 7. Optionally delete loser canonical_entity
pub async fn execute_merge(
    conn: &mut PgConnection,
    req: &MergeRequest,
    opts: &MergeOptions,
) -> crate::error::Result<MergeResult> {
    // 1. Verify winner != loser
    if req.winner_canonical_id == req.loser_canonical_id {
        return Err(crate::error::CannonError::Conflict(
            "Cannot merge entity with itself".into(),
        ));
    }

    // 2. Count loser's members before moving them
    let members_moved: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM identity_links WHERE canonical_entity_id = $1 AND tenant_id = $2",
    )
    .bind(req.loser_canonical_id)
    .bind(req.tenant_id)
    .fetch_one(&mut *conn)
    .await
    .unwrap_or(0);

    // 3. Move identity_links from loser to winner
    sqlx::query(
        "UPDATE identity_links SET canonical_entity_id = $1 \
         WHERE canonical_entity_id = $2 AND tenant_id = $3",
    )
    .bind(req.winner_canonical_id)
    .bind(req.loser_canonical_id)
    .bind(req.tenant_id)
    .execute(&mut *conn)
    .await?;

    // 4. Record entity_merges
    sqlx::query(
        "INSERT INTO entity_merges \
             (tenant_id, winner_entity_id, loser_entity_id, merge_type, confidence) \
         VALUES ($1, $2, $3, $4, $5)",
    )
    .bind(req.tenant_id)
    .bind(req.winner_canonical_id)
    .bind(req.loser_canonical_id)
    .bind(req.merge_type.as_str())
    .bind(req.confidence)
    .execute(&mut *conn)
    .await?;

    // 5. Emit entity.merged event
    let mut event_payload = serde_json::json!({
        "winner": req.winner_canonical_id.to_string(),
        "loser": req.loser_canonical_id.to_string(),
        "merge_type": req.merge_type.as_str(),
        "members_moved": members_moved,
    });
    if let Some(conf) = req.confidence {
        event_payload["confidence"] = serde_json::json!(conf);
    }
    // Merge caller's metadata into the event payload
    if let Some(obj) = req.metadata.as_object() {
        for (k, v) in obj {
            event_payload[k] = v.clone();
        }
    }

    sqlx::query(
        "INSERT INTO entity_events (tenant_id, entity_id, event_type, payload) \
         VALUES ($1, $2, 'entity.merged', $3)",
    )
    .bind(req.tenant_id)
    .bind(req.winner_canonical_id)
    .bind(&event_payload)
    .execute(&mut *conn)
    .await?;

    // 6. Optionally recompute canonical_data
    if opts.recompute_canonical {
        recompute_canonical_data(conn, req.tenant_id, req.winner_canonical_id).await?;
    }

    // 7. Optionally delete loser
    if opts.delete_loser {
        sqlx::query(
            "DELETE FROM canonical_entities WHERE id = $1 AND tenant_id = $2",
        )
        .bind(req.loser_canonical_id)
        .bind(req.tenant_id)
        .execute(&mut *conn)
        .await?;
    }

    // Count total members on winner after merge
    let total_members: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM identity_links WHERE canonical_entity_id = $1 AND tenant_id = $2",
    )
    .bind(req.winner_canonical_id)
    .bind(req.tenant_id)
    .fetch_one(&mut *conn)
    .await
    .unwrap_or(0);

    Ok(MergeResult {
        winner_id: req.winner_canonical_id,
        loser_id: req.loser_canonical_id,
        members_moved,
        total_members,
    })
}

/// Recompute canonical_data for an entity using last-non-null survivorship.
/// Members are iterated in external_id order for determinism.
async fn recompute_canonical_data(
    conn: &mut PgConnection,
    tenant_id: Uuid,
    canonical_id: Uuid,
) -> crate::error::Result<()> {
    let members = sqlx::query(
        "SELECT ee.raw_data, ee.external_id \
         FROM identity_links il \
         JOIN external_entities ee ON il.external_entity_id = ee.id \
         WHERE il.canonical_entity_id = $1 AND il.tenant_id = $2 \
         ORDER BY ee.external_id",
    )
    .bind(canonical_id)
    .bind(tenant_id)
    .fetch_all(&mut *conn)
    .await?;

    if members.is_empty() {
        return Ok(());
    }

    let mut golden_data = serde_json::Map::new();
    for member in &members {
        let raw: serde_json::Value = member.get("raw_data");
        if let Some(obj) = raw.as_object() {
            for (k, v) in obj {
                if !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true) {
                    golden_data.insert(k.clone(), v.clone());
                }
            }
        }
    }

    sqlx::query(
        "UPDATE canonical_entities SET canonical_data = $2, updated_at = NOW() \
         WHERE id = $1 AND tenant_id = $3",
    )
    .bind(canonical_id)
    .bind(serde_json::Value::Object(golden_data))
    .bind(tenant_id)
    .execute(&mut *conn)
    .await?;

    Ok(())
}

/// Resolve external entity IDs to their canonical entity IDs.
///
/// Returns `(canonical_a, canonical_b)`. Errors if either entity has no canonical link.
pub async fn resolve_canonical_ids(
    conn: &mut PgConnection,
    tenant_id: Uuid,
    entity_a_id: Uuid,
    entity_b_id: Uuid,
) -> crate::error::Result<(Uuid, Uuid)> {
    let canonical_a: Option<Uuid> = sqlx::query_scalar(
        "SELECT canonical_entity_id FROM identity_links \
         WHERE external_entity_id = $1 AND tenant_id = $2",
    )
    .bind(entity_a_id)
    .bind(tenant_id)
    .fetch_optional(&mut *conn)
    .await?;

    let canonical_b: Option<Uuid> = sqlx::query_scalar(
        "SELECT canonical_entity_id FROM identity_links \
         WHERE external_entity_id = $1 AND tenant_id = $2",
    )
    .bind(entity_b_id)
    .bind(tenant_id)
    .fetch_optional(&mut *conn)
    .await?;

    match (canonical_a, canonical_b) {
        (Some(a), Some(b)) => Ok((a, b)),
        (None, _) => Err(crate::error::CannonError::NotFound(format!(
            "Entity {} has no canonical link",
            entity_a_id
        ))),
        (_, None) => Err(crate::error::CannonError::NotFound(format!(
            "Entity {} has no canonical link",
            entity_b_id
        ))),
    }
}

/// Determine winner and loser between two canonical entities.
///
/// Winner has more members. Ties broken by `canonical_id.to_string()` comparison
/// (smaller string wins) for determinism.
///
/// Returns `(winner, loser)`.
pub async fn determine_winner(
    conn: &mut PgConnection,
    tenant_id: Uuid,
    canonical_a: Uuid,
    canonical_b: Uuid,
) -> crate::error::Result<(Uuid, Uuid)> {
    let count_a: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM identity_links WHERE canonical_entity_id = $1 AND tenant_id = $2",
    )
    .bind(canonical_a)
    .bind(tenant_id)
    .fetch_one(&mut *conn)
    .await
    .unwrap_or(0);

    let count_b: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM identity_links WHERE canonical_entity_id = $1 AND tenant_id = $2",
    )
    .bind(canonical_b)
    .bind(tenant_id)
    .fetch_one(&mut *conn)
    .await
    .unwrap_or(0);

    let (winner, loser) = if count_a > count_b {
        (canonical_a, canonical_b)
    } else if count_b > count_a {
        (canonical_b, canonical_a)
    } else if canonical_a.to_string() < canonical_b.to_string() {
        (canonical_a, canonical_b)
    } else {
        (canonical_b, canonical_a)
    };

    Ok((winner, loser))
}

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

    #[test]
    fn test_merge_type_as_str() {
        assert_eq!(MergeType::Auto.as_str(), "auto");
        assert_eq!(MergeType::Realtime.as_str(), "realtime");
        assert_eq!(MergeType::Agent.as_str(), "agent");
        assert_eq!(MergeType::Manual.as_str(), "manual");
    }

    #[test]
    fn test_merge_type_display() {
        assert_eq!(format!("{}", MergeType::Agent), "agent");
        assert_eq!(format!("{}", MergeType::Manual), "manual");
    }

    #[test]
    fn test_merge_options_default() {
        let opts = MergeOptions::default();
        assert!(!opts.delete_loser);
        assert!(opts.recompute_canonical);
    }
}
