//! Messaging GraphQL mutations
//!
//! Mutations for conversations, messages, and participants.

use async_graphql::{Context, InputObject, Object, Result};
use chrono::Utc;
use serde_json::Value as JsonValue;
use sqlx::PgPool;
use uuid::Uuid;

use crate::auth::{ProfileType, UserContext};
use crate::db::Database;
use crate::graphql::types::{
    ConversationParticipantType, ConversationType_, ConversationTypeEnum, MessageType,
};
use crate::models::{Conversation, ConversationParticipant, ConversationType, Message};

// ==================== HELPER FUNCTIONS ====================

/// Get participant name based on type and ID
async fn get_participant_name(
    pool: &PgPool,
    participant_type: &str,
    participant_id: Uuid,
) -> Result<String> {
    let name: Option<String> = match participant_type {
        "team_profile" => {
            sqlx::query_scalar(
                r#"SELECT first_name || ' ' || last_name FROM team_profiles WHERE id = $1"#,
            )
            .bind(participant_id)
            .fetch_optional(pool)
            .await?
        }
        "customer_profile" => {
            sqlx::query_scalar(
                r#"SELECT first_name || ' ' || last_name FROM customer_profiles WHERE id = $1"#,
            )
            .bind(participant_id)
            .fetch_optional(pool)
            .await?
        }
        _ => None,
    };

    Ok(name.unwrap_or_else(|| "Unknown".to_string()))
}

/// Insert a system message into a conversation
async fn insert_system_message(
    pool: &PgPool,
    conversation_id: Uuid,
    content: &str,
) -> Result<()> {
    let now = Utc::now();

    sqlx::query(
        r#"
        INSERT INTO messages (
            id, created_at, updated_at, conversation_id, author_type, author_id,
            content, is_deleted, reply_to_id, attachments, is_system_message, metadata
        )
        VALUES (gen_random_uuid(), $1, $1, $2, NULL, NULL, $3, false, NULL, '[]'::jsonb, true, '{}'::jsonb)
        "#,
    )
    .bind(now)
    .bind(conversation_id)
    .bind(content)
    .execute(pool)
    .await?;

    // Update conversation's last_message_at
    sqlx::query(
        r#"UPDATE conversations SET last_message_at = $1, updated_at = $1 WHERE id = $2"#,
    )
    .bind(now)
    .bind(conversation_id)
    .execute(pool)
    .await?;

    Ok(())
}

/// Get the first admin team profile ID (dispatch)
async fn get_dispatch_profile_id(pool: &PgPool) -> Result<Option<Uuid>> {
    let id: Option<Uuid> = sqlx::query_scalar(
        r#"SELECT id FROM team_profiles WHERE role = 'ADMIN' AND status = 'ACTIVE' ORDER BY created_at LIMIT 1"#,
    )
    .fetch_optional(pool)
    .await?;

    Ok(id)
}

/// Check if conversation should be auto-archived (no active participants)
async fn check_and_archive_conversation(pool: &PgPool, conversation_id: Uuid) -> Result<bool> {
    let remaining: i64 = sqlx::query_scalar(
        r#"
        SELECT COUNT(*) FROM conversation_participants
        WHERE conversation_id = $1 AND is_archived = false
        "#,
    )
    .bind(conversation_id)
    .fetch_one(pool)
    .await?;

    if remaining == 0 {
        // Archive the conversation
        sqlx::query(
            r#"UPDATE conversations SET is_archived = true, updated_at = NOW() WHERE id = $1"#,
        )
        .bind(conversation_id)
        .execute(pool)
        .await?;

        // Insert final system message
        insert_system_message(pool, conversation_id, "Conversation archived - no remaining participants").await?;

        return Ok(true);
    }

    Ok(false)
}

// ==================== INPUT TYPES ====================

/// Input for creating a new conversation
#[derive(InputObject)]
pub struct CreateConversationInput {
    /// Optional subject line
    pub subject: Option<String>,
    /// Type of conversation
    pub conversation_type: ConversationTypeEnum,
    /// Entity type this conversation is about (optional)
    pub entity_type: Option<String>,
    /// Entity ID this conversation is about (optional)
    pub entity_id: Option<Uuid>,
    /// Initial participant IDs (will include creator automatically)
    pub participant_ids: Vec<ParticipantInput>,
    /// Initial message content (optional)
    pub initial_message: Option<String>,
}

/// Input for specifying a participant
#[derive(InputObject, Clone)]
pub struct ParticipantInput {
    pub participant_type: String,
    pub participant_id: Uuid,
}

/// Input for sending a message
#[derive(InputObject)]
pub struct SendMessageInput {
    pub conversation_id: Uuid,
    pub content: String,
    pub reply_to_id: Option<Uuid>,
    pub attachments: Option<JsonValue>,
}

/// Input for updating a conversation
#[derive(InputObject)]
pub struct UpdateConversationInput {
    pub subject: Option<String>,
}

// ==================== MUTATION IMPLEMENTATION ====================

#[derive(Default)]
pub struct MessagingMutation;

#[Object]
impl MessagingMutation {
    // ==================== CONVERSATION MUTATIONS ====================

    /// Create a new conversation
    async fn create_conversation(
        &self,
        ctx: &Context<'_>,
        input: CreateConversationInput,
    ) -> Result<ConversationType_> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let created_by_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        let conversation_type = match input.conversation_type {
            ConversationTypeEnum::Direct => ConversationType::Direct,
            ConversationTypeEnum::Group => ConversationType::Group,
            ConversationTypeEnum::Support => ConversationType::Support,
        };

        let now = Utc::now();

        // Create the conversation
        let conversation: Conversation = sqlx::query_as(
            r#"
            INSERT INTO conversations (
                id, created_at, updated_at, subject, conversation_type, entity_type, entity_id,
                created_by_type, created_by_id, last_message_at, is_archived, metadata
            )
            VALUES (
                gen_random_uuid(), $1, $1, $2, $3, $4, $5, $6, $7, NULL, false, '{}'::jsonb
            )
            RETURNING id, created_at, updated_at, subject, conversation_type, entity_type, entity_id,
                      created_by_type, created_by_id, last_message_at, is_archived, metadata
            "#,
        )
        .bind(now)
        .bind(&input.subject)
        .bind(conversation_type)
        .bind(&input.entity_type)
        .bind(input.entity_id)
        .bind(created_by_type)
        .bind(user.user_id)
        .fetch_one(&*pool)
        .await?;

        // Add the creator as a participant
        sqlx::query(
            r#"
            INSERT INTO conversation_participants (
                id, created_at, updated_at, conversation_id, participant_type, participant_id,
                last_read_at, unread_count, is_muted, is_archived, joined_at
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $1, 0, false, false, $1)
            "#,
        )
        .bind(now)
        .bind(conversation.base.id)
        .bind(created_by_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        // Add other participants
        for participant in &input.participant_ids {
            // Skip if this is the creator (already added)
            if participant.participant_type == created_by_type
                && participant.participant_id == user.user_id
            {
                continue;
            }

            sqlx::query(
                r#"
                INSERT INTO conversation_participants (
                    id, created_at, updated_at, conversation_id, participant_type, participant_id,
                    last_read_at, unread_count, is_muted, is_archived, joined_at
                )
                VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, NULL, 0, false, false, $1)
                "#,
            )
            .bind(now)
            .bind(conversation.base.id)
            .bind(&participant.participant_type)
            .bind(participant.participant_id)
            .execute(&*pool)
            .await?;
        }

        // For SUPPORT conversations, auto-add dispatch (admin) if not already included
        if conversation_type == ConversationType::Support {
            if let Some(dispatch_id) = get_dispatch_profile_id(&pool).await? {
                // Check if dispatch is already a participant
                let dispatch_already_added = input
                    .participant_ids
                    .iter()
                    .any(|p| p.participant_type == "team_profile" && p.participant_id == dispatch_id)
                    || (created_by_type == "team_profile" && user.user_id == dispatch_id);

                if !dispatch_already_added {
                    sqlx::query(
                        r#"
                        INSERT INTO conversation_participants (
                            id, created_at, updated_at, conversation_id, participant_type, participant_id,
                            last_read_at, unread_count, is_muted, is_archived, joined_at
                        )
                        VALUES (gen_random_uuid(), $1, $1, $2, 'team_profile', $3, NULL, 0, false, false, $1)
                        "#,
                    )
                    .bind(now)
                    .bind(conversation.base.id)
                    .bind(dispatch_id)
                    .execute(&*pool)
                    .await?;
                }
            }
        }

        // If there's an initial message, send it
        if let Some(content) = input.initial_message {
            sqlx::query(
                r#"
                INSERT INTO messages (
                    id, created_at, updated_at, conversation_id, author_type, author_id,
                    content, is_deleted, reply_to_id, attachments, is_system_message, metadata
                )
                VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, false, NULL, '[]'::jsonb, false, '{}'::jsonb)
                "#,
            )
            .bind(now)
            .bind(conversation.base.id)
            .bind(created_by_type)
            .bind(user.user_id)
            .bind(&content)
            .execute(&*pool)
            .await?;

            // Update last_message_at and increment unread counts for other participants
            sqlx::query(
                r#"
                UPDATE conversations SET last_message_at = $1, updated_at = $1 WHERE id = $2
                "#,
            )
            .bind(now)
            .bind(conversation.base.id)
            .execute(&*pool)
            .await?;

            sqlx::query(
                r#"
                UPDATE conversation_participants
                SET unread_count = unread_count + 1, updated_at = $1
                WHERE conversation_id = $2
                  AND NOT (participant_type = $3 AND participant_id = $4)
                "#,
            )
            .bind(now)
            .bind(conversation.base.id)
            .bind(created_by_type)
            .bind(user.user_id)
            .execute(&*pool)
            .await?;
        }

        Ok(ConversationType_::from(conversation))
    }

    /// Update a conversation
    async fn update_conversation(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        input: UpdateConversationInput,
    ) -> Result<ConversationType_> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let conversation: Conversation = sqlx::query_as(
            r#"
            UPDATE conversations
            SET subject = COALESCE($1, subject),
                updated_at = NOW()
            WHERE id = $2
            RETURNING id, created_at, updated_at, subject, conversation_type, entity_type, entity_id,
                      created_by_type, created_by_id, last_message_at, is_archived, metadata
            "#,
        )
        .bind(&input.subject)
        .bind(id)
        .fetch_one(&*pool)
        .await?;

        Ok(ConversationType_::from(conversation))
    }

    /// Archive or unarchive a conversation
    async fn archive_conversation(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        is_archived: bool,
    ) -> Result<ConversationType_> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let conversation: Conversation = sqlx::query_as(
            r#"
            UPDATE conversations
            SET is_archived = $1, updated_at = NOW()
            WHERE id = $2
            RETURNING id, created_at, updated_at, subject, conversation_type, entity_type, entity_id,
                      created_by_type, created_by_id, last_message_at, is_archived, metadata
            "#,
        )
        .bind(is_archived)
        .bind(id)
        .fetch_one(&*pool)
        .await?;

        Ok(ConversationType_::from(conversation))
    }

    /// Mute or unmute a conversation for the current user
    async fn mute_conversation(
        &self,
        ctx: &Context<'_>,
        id: Uuid,
        is_muted: bool,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let participant_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        let result = sqlx::query(
            r#"
            UPDATE conversation_participants
            SET is_muted = $1, updated_at = NOW()
            WHERE conversation_id = $2 AND participant_type = $3 AND participant_id = $4
            "#,
        )
        .bind(is_muted)
        .bind(id)
        .bind(participant_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== MESSAGE MUTATIONS ====================

    /// Send a message to a conversation
    async fn send_message(&self, ctx: &Context<'_>, input: SendMessageInput) -> Result<MessageType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let author_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        let now = Utc::now();

        // Verify user is a participant
        let is_participant: bool = sqlx::query_scalar(
            r#"
            SELECT EXISTS(
                SELECT 1 FROM conversation_participants
                WHERE conversation_id = $1 AND participant_type = $2 AND participant_id = $3 AND is_archived = false
            )
            "#,
        )
        .bind(input.conversation_id)
        .bind(author_type)
        .bind(user.user_id)
        .fetch_one(&*pool)
        .await?;

        if !is_participant {
            return Err(async_graphql::Error::new(
                "You are not a participant in this conversation",
            ));
        }

        // Create the message
        let message: Message = sqlx::query_as(
            r#"
            INSERT INTO messages (
                id, created_at, updated_at, conversation_id, author_type, author_id,
                content, is_deleted, reply_to_id, attachments, is_system_message, metadata
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, $5, false, $6, COALESCE($7, '[]'::jsonb), false, '{}'::jsonb)
            RETURNING id, created_at, updated_at, conversation_id, author_type, author_id,
                      content, is_deleted, reply_to_id, attachments, is_system_message, metadata
            "#,
        )
        .bind(now)
        .bind(input.conversation_id)
        .bind(author_type)
        .bind(user.user_id)
        .bind(&input.content)
        .bind(input.reply_to_id)
        .bind(&input.attachments)
        .fetch_one(&*pool)
        .await?;

        // Update conversation's last_message_at
        sqlx::query(
            r#"
            UPDATE conversations SET last_message_at = $1, updated_at = $1 WHERE id = $2
            "#,
        )
        .bind(now)
        .bind(input.conversation_id)
        .execute(&*pool)
        .await?;

        // Increment unread count for other participants
        sqlx::query(
            r#"
            UPDATE conversation_participants
            SET unread_count = unread_count + 1, updated_at = $1
            WHERE conversation_id = $2
              AND is_archived = false
              AND NOT (participant_type = $3 AND participant_id = $4)
            "#,
        )
        .bind(now)
        .bind(input.conversation_id)
        .bind(author_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        // Mark conversation as read for the sender
        sqlx::query(
            r#"
            UPDATE conversation_participants
            SET last_read_at = $1, unread_count = 0, updated_at = $1
            WHERE conversation_id = $2 AND participant_type = $3 AND participant_id = $4
            "#,
        )
        .bind(now)
        .bind(input.conversation_id)
        .bind(author_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        Ok(MessageType::from(message))
    }

    /// Delete (soft-delete) a message
    async fn delete_message(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let author_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        // Only the author can delete their own message
        let result = sqlx::query(
            r#"
            UPDATE messages
            SET is_deleted = true, updated_at = NOW()
            WHERE id = $1 AND author_type = $2 AND author_id = $3
            "#,
        )
        .bind(id)
        .bind(author_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        Ok(result.rows_affected() > 0)
    }

    // ==================== READ TRACKING MUTATIONS ====================

    /// Mark a conversation as read for the current user
    async fn mark_conversation_as_read(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let participant_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        let now = Utc::now();

        let result = sqlx::query(
            r#"
            UPDATE conversation_participants
            SET last_read_at = $1, unread_count = 0, updated_at = $1
            WHERE conversation_id = $2 AND participant_type = $3 AND participant_id = $4
            "#,
        )
        .bind(now)
        .bind(id)
        .bind(participant_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        Ok(result.rows_affected() > 0)
    }

    /// Mark all conversations as read for the current user
    async fn mark_all_conversations_as_read(&self, ctx: &Context<'_>) -> Result<i32> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let participant_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        let now = Utc::now();

        let result = sqlx::query(
            r#"
            UPDATE conversation_participants
            SET last_read_at = $1, unread_count = 0, updated_at = $1
            WHERE participant_type = $2 AND participant_id = $3 AND unread_count > 0
            "#,
        )
        .bind(now)
        .bind(participant_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        Ok(result.rows_affected() as i32)
    }

    // ==================== PARTICIPANT MUTATIONS ====================

    /// Add a participant to a conversation
    async fn add_participant(
        &self,
        ctx: &Context<'_>,
        conversation_id: Uuid,
        participant: ParticipantInput,
    ) -> Result<ConversationParticipantType> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        let now = Utc::now();

        // Check if participant already exists (even if archived)
        let existing: Option<bool> = sqlx::query_scalar(
            r#"
            SELECT is_archived FROM conversation_participants
            WHERE conversation_id = $1 AND participant_type = $2 AND participant_id = $3
            "#,
        )
        .bind(conversation_id)
        .bind(&participant.participant_type)
        .bind(participant.participant_id)
        .fetch_optional(&*pool)
        .await?;

        let cp: ConversationParticipant = sqlx::query_as(
            r#"
            INSERT INTO conversation_participants (
                id, created_at, updated_at, conversation_id, participant_type, participant_id,
                last_read_at, unread_count, is_muted, is_archived, joined_at
            )
            VALUES (gen_random_uuid(), $1, $1, $2, $3, $4, NULL, 0, false, false, $1)
            ON CONFLICT (conversation_id, participant_type, participant_id)
            DO UPDATE SET is_archived = false, updated_at = $1
            RETURNING id, created_at, updated_at, conversation_id, participant_type, participant_id,
                      last_read_at, unread_count, is_muted, is_archived, joined_at
            "#,
        )
        .bind(now)
        .bind(conversation_id)
        .bind(&participant.participant_type)
        .bind(participant.participant_id)
        .fetch_one(&*pool)
        .await?;

        // Create system message for join
        // Only if this is a new participant OR was previously archived (rejoining)
        let should_create_message = existing.is_none() || existing == Some(true);
        if should_create_message {
            let name = get_participant_name(&pool, &participant.participant_type, participant.participant_id).await?;
            insert_system_message(&pool, conversation_id, &format!("{} joined the conversation", name)).await?;
        }

        Ok(ConversationParticipantType::from(cp))
    }

    /// Remove a participant from a conversation (soft-remove)
    async fn remove_participant(
        &self,
        ctx: &Context<'_>,
        conversation_id: Uuid,
        participant: ParticipantInput,
    ) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;

        // Check if participant is actually active
        let is_active_participant: bool = sqlx::query_scalar(
            r#"
            SELECT EXISTS(
                SELECT 1 FROM conversation_participants
                WHERE conversation_id = $1 AND participant_type = $2 AND participant_id = $3 AND is_archived = false
            )
            "#,
        )
        .bind(conversation_id)
        .bind(&participant.participant_type)
        .bind(participant.participant_id)
        .fetch_one(&*pool)
        .await?;

        if !is_active_participant {
            return Ok(false);
        }

        // Get participant name for system message
        let name = get_participant_name(&pool, &participant.participant_type, participant.participant_id).await?;

        // Create system message
        insert_system_message(&pool, conversation_id, &format!("{} was removed from the conversation", name)).await?;

        // Soft-remove the participant
        let result = sqlx::query(
            r#"
            UPDATE conversation_participants
            SET is_archived = true, updated_at = NOW()
            WHERE conversation_id = $1 AND participant_type = $2 AND participant_id = $3
            "#,
        )
        .bind(conversation_id)
        .bind(&participant.participant_type)
        .bind(participant.participant_id)
        .execute(&*pool)
        .await?;

        // Check if conversation should be auto-archived
        check_and_archive_conversation(&pool, conversation_id).await?;

        Ok(result.rows_affected() > 0)
    }

    /// Leave a conversation (current user removes themselves)
    async fn leave_conversation(&self, ctx: &Context<'_>, conversation_id: Uuid) -> Result<bool> {
        let db = ctx.data::<Database>()?;
        let pool = db.pool().await;
        let user = ctx.data::<UserContext>()?;

        let participant_type = match user.profile_type {
            ProfileType::Team => "team_profile",
            ProfileType::Customer => "customer_profile",
        };

        // Check if user is actually a participant (and not already archived)
        let is_active_participant: bool = sqlx::query_scalar(
            r#"
            SELECT EXISTS(
                SELECT 1 FROM conversation_participants
                WHERE conversation_id = $1 AND participant_type = $2 AND participant_id = $3 AND is_archived = false
            )
            "#,
        )
        .bind(conversation_id)
        .bind(participant_type)
        .bind(user.user_id)
        .fetch_one(&*pool)
        .await?;

        if !is_active_participant {
            return Ok(false);
        }

        // Get participant name for system message
        let name = get_participant_name(&pool, participant_type, user.user_id).await?;

        // Create system message before leaving
        insert_system_message(&pool, conversation_id, &format!("{} left the conversation", name)).await?;

        // Soft-remove the participant
        let result = sqlx::query(
            r#"
            UPDATE conversation_participants
            SET is_archived = true, updated_at = NOW()
            WHERE conversation_id = $1 AND participant_type = $2 AND participant_id = $3
            "#,
        )
        .bind(conversation_id)
        .bind(participant_type)
        .bind(user.user_id)
        .execute(&*pool)
        .await?;

        // Check if conversation should be auto-archived
        check_and_archive_conversation(&pool, conversation_id).await?;

        Ok(result.rows_affected() > 0)
    }
}
