use anyhow::Result;
use diesel::prelude::*;
use diesel::query_dsl::BelongingToDsl;
use std::collections::HashMap;

use crate::{
    models::{
        db::conversation::Record as ConversationRecord,
        db::message::Record as MessageRecord,
        db::participant::{
            ConversationParticipant, InsertableRecord as InsertableParticipantRecord,
            Record as ParticipantRecord,
        },
        Conversation, Message, Participant,
    },
    schema, target,
};

pub struct Repository<'a> {
    connection: &'a mut SqliteConnection,
}

impl<'a> Repository<'a> {
    pub fn new(connection: &'a mut SqliteConnection) -> Self {
        Self { connection }
    }

    pub fn insert_conversation(&mut self, conversation: Conversation) -> Result<()> {
        use crate::schema::conversation_participants::dsl::*;
        use crate::schema::conversations::dsl::*;
        use crate::schema::participants::dsl::*;

        let (db_conversation, db_participants) = conversation.into();

        diesel::replace_into(conversations)
            .values(&db_conversation)
            .execute(self.connection)?;

        for participant in &db_participants {
            diesel::insert_into(participants)
                .values(participant)
                .on_conflict_do_nothing()
                .execute(self.connection)?;
        }

        // Sqlite backend doesn't support batch insert, so we have to do this manually
        for participant in &db_participants {
            diesel::replace_into(conversation_participants)
                .values((
                    conversation_id.eq(&db_conversation.id),
                    participant_handle.eq(&participant.handle),
                ))
                .execute(self.connection)?;
        }

        Ok(())
    }

    pub fn get_conversation_by_guid(&mut self, match_guid: &str) -> Result<Option<Conversation>> {
        use crate::schema::conversations::dsl::*;
        use crate::schema::participants::dsl::*;

        let result = conversations
            .find(match_guid)
            .first::<ConversationRecord>(self.connection)
            .optional()?;

        if let Some(conversation) = result {
            let db_participants = ConversationParticipant::belonging_to(&conversation)
                .inner_join(participants)
                .select(ParticipantRecord::as_select())
                .load::<ParticipantRecord>(self.connection)?;

            let mut model_conversation: Conversation = conversation.into();
            model_conversation.participants =
                db_participants.into_iter().map(|p| p.into()).collect();

            return Ok(Some(model_conversation));
        }

        Ok(None)
    }

    pub fn all_conversations(&mut self, limit: i32, offset: i32) -> Result<Vec<Conversation>> {
        use crate::schema::conversations::dsl::*;
        use crate::schema::participants::dsl::*;

        let db_conversations = conversations
            .order(schema::conversations::date.desc())
            .offset(offset as i64)
            .limit(limit as i64)
            .load::<ConversationRecord>(self.connection)?;

        let mut result = Vec::new();
        for db_conversation in db_conversations {
            let db_participants = ConversationParticipant::belonging_to(&db_conversation)
                .inner_join(participants)
                .select(ParticipantRecord::as_select())
                .load::<ParticipantRecord>(self.connection)?;

            let mut model_conversation: Conversation = db_conversation.into();
            model_conversation.participants =
                db_participants.into_iter().map(|p| p.into()).collect();

            result.push(model_conversation);
        }

        Ok(result)
    }

    pub fn insert_message(&mut self, conversation_guid: &str, message: Message) -> Result<()> {
        use crate::schema::conversation_messages::dsl::*;
        use crate::schema::messages::dsl::*;

        // Handle participant if message has a remote sender
        let sender = message.sender.clone();
        let mut db_message: MessageRecord = message.into();
        db_message.sender_participant_handle = self.get_or_create_participant(&sender);

        diesel::replace_into(messages)
            .values(&db_message)
            .execute(self.connection)?;

        diesel::replace_into(conversation_messages)
            .values((
                conversation_id.eq(conversation_guid),
                message_id.eq(&db_message.id),
            ))
            .execute(self.connection)?;

        // Update conversation date
        self.update_conversation_metadata(conversation_guid)?;

        Ok(())
    }

    pub fn insert_messages(
        &mut self,
        conversation_guid: &str,
        in_messages: Vec<Message>,
    ) -> Result<()> {
        use crate::schema::conversation_messages::dsl::*;
        use crate::schema::messages::dsl::*;
        use crate::schema::participants::dsl as participants_dsl;

        #[derive(Insertable)]
        #[diesel(table_name = crate::schema::conversation_messages)]
        struct InsertableConversationMessage {
            pub conversation_id: String,
            pub message_id: String,
        }

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

        // Use a single transaction for everything – this removes the implicit
        // autocommit after every statement which costs a lot when we have many
        // individual queries.
        self.connection
            .transaction::<_, diesel::result::Error, _>(|conn| {
                // Cache participant handles we have already looked up / created – in a
                // typical conversation we only have a handful of participants, but we
                // might be processing hundreds of messages. Avoiding an extra SELECT per
                // message saves a lot of round-trips to SQLite.
                let mut participant_cache: HashMap<String, String> = HashMap::new();

                // Prepare collections for the batch inserts.
                let mut db_messages: Vec<MessageRecord> = Vec::with_capacity(in_messages.len());
                let mut conv_msg_records: Vec<InsertableConversationMessage> =
                    Vec::with_capacity(in_messages.len());

                for message in in_messages {
                    // Resolve/insert the sender participant only once per display name.
                    let sender_handle_opt = match &message.sender {
                        Participant::Me => None,
                        Participant::Remote { handle, contact_id } => {
                            if participant_cache.contains_key(handle) {
                                Some(handle.clone())
                            } else {
                                // Ensure participant exists in DB
                                let exists: Option<String> = participants_dsl::participants
                                    .filter(participants_dsl::handle.eq(handle))
                                    .select(participants_dsl::handle)
                                    .first::<String>(conn)
                                    .optional()?;

                                if exists.is_none() {
                                    let new_participant = InsertableParticipantRecord {
                                        handle: handle.clone(),
                                        is_me: false,
                                        contact_id: contact_id.clone(),
                                    };

                                    diesel::insert_into(participants_dsl::participants)
                                        .values(&new_participant)
                                        .execute(conn)?;
                                }

                                participant_cache.insert(handle.clone(), handle.clone());
                                Some(handle.clone())
                            }
                        }
                    };

                    // Convert the message into its DB form.
                    let mut db_message: MessageRecord = message.into();
                    db_message.sender_participant_handle = sender_handle_opt.clone();

                    conv_msg_records.push(InsertableConversationMessage {
                        conversation_id: conversation_guid.to_string(),
                        message_id: db_message.id.clone(),
                    });

                    db_messages.push(db_message);
                }

                // Execute the actual batch inserts.
                diesel::replace_into(messages)
                    .values(&db_messages)
                    .execute(conn)?;

                diesel::replace_into(conversation_messages)
                    .values(&conv_msg_records)
                    .execute(conn)?;

                // Update conversation metadata quickly using the last message we just
                // processed instead of re-querying the DB.
                if let Some(last_msg) = db_messages.last() {
                    use crate::schema::conversations::dsl as conv_dsl;
                    diesel::update(
                        conv_dsl::conversations.filter(conv_dsl::id.eq(conversation_guid)),
                    )
                    .set((
                        conv_dsl::date.eq(last_msg.date),
                        conv_dsl::last_message_preview
                            .eq::<Option<String>>(Some(last_msg.text.clone())),
                    ))
                    .execute(conn)?;
                }

                Ok(())
            })?;

        // TODO: May need to update conversation metadata here, but this has a perf impact.
        // Ideally we would consolidate this in the code above, assuming we're only inserting *new* messages, but
        // this may not necessarily be the case.

        Ok(())
    }

    pub fn get_messages_for_conversation(
        &mut self,
        conversation_guid: &str,
    ) -> Result<Vec<Message>> {
        use crate::schema::conversation_messages::dsl::*;
        use crate::schema::messages::dsl::*;
        use crate::schema::participants::dsl::*;

        let message_records = conversation_messages
            .filter(conversation_id.eq(conversation_guid))
            .inner_join(messages)
            .select(MessageRecord::as_select())
            .order_by(schema::messages::date.asc())
            .load::<MessageRecord>(self.connection)?;

        let sender_handles: Vec<String> = message_records
            .iter()
            .filter_map(|record| record.sender_participant_handle.clone())
            .collect();

        let participant_map: HashMap<String, Participant> = if sender_handles.is_empty() {
            HashMap::new()
        } else {
            participants
                .filter(handle.eq_any(&sender_handles))
                .load::<ParticipantRecord>(self.connection)?
                .into_iter()
                .map(|participant| {
                    let key = participant.handle.clone();
                    (key, participant.into())
                })
                .collect()
        };

        let mut result = Vec::new();
        for message_record in message_records {
            let mut message: Message = message_record.clone().into();

            // If the message references a sender participant, load the participant info
            if let Some(sender_handle) = message_record.sender_participant_handle {
                if let Some(participant) = participant_map.get(&sender_handle) {
                    message.sender = participant.clone();
                }
            }

            result.push(message);
        }

        Ok(result)
    }

    pub fn get_last_message_for_conversation(
        &mut self,
        conversation_guid: &str,
    ) -> Result<Option<Message>> {
        use crate::schema::conversation_messages::dsl::*;
        use crate::schema::messages::dsl::*;

        let message_record = conversation_messages
            .filter(conversation_id.eq(conversation_guid))
            .inner_join(messages)
            .select(MessageRecord::as_select())
            .order_by(schema::messages::date.desc())
            .first::<MessageRecord>(self.connection)
            .optional()?;

        Ok(message_record.map(|r| r.into()))
    }

    pub fn delete_all_conversations(&mut self) -> Result<()> {
        use crate::schema::conversations::dsl::*;
        diesel::delete(conversations).execute(self.connection)?;
        Ok(())
    }

    pub fn delete_all_messages(&mut self) -> Result<()> {
        use crate::schema::message_aliases::dsl as aliases_dsl;
        use crate::schema::messages::dsl as messages_dsl;

        diesel::delete(messages_dsl::messages).execute(self.connection)?;
        diesel::delete(aliases_dsl::message_aliases).execute(self.connection)?;
        Ok(())
    }

    pub fn merge_conversation_metadata(&mut self, in_conversation: Conversation) -> Result<bool> {
        let mut updated = false;
        let conversation = self.get_conversation_by_guid(&in_conversation.guid)?;
        if let Some(conversation) = conversation {
            let merged_conversation = conversation.merge(&in_conversation, None);

            if merged_conversation != conversation {
                self.insert_conversation(merged_conversation)?;
                updated = true;
            }
        }

        log::debug!(target: target::REPOSITORY, "Merged conversation metadata: {} updated: {}", in_conversation.guid, updated);
        Ok(updated)
    }

    fn update_conversation_metadata(&mut self, conversation_guid: &str) -> Result<()> {
        let conversation = self.get_conversation_by_guid(conversation_guid)?;
        if let Some(conversation) = conversation {
            if let Some(last_message) = self.get_last_message_for_conversation(conversation_guid)? {
                log::debug!(
                    target: target::REPOSITORY,
                    "Updating conversation metadata: {} message: {:?}",
                    conversation_guid,
                    last_message
                );

                let merged_conversation = conversation.merge(&conversation, Some(&last_message));
                self.insert_conversation(merged_conversation)?;
            }
        }

        Ok(())
    }

    // Helper function to get the last inserted row ID
    // This is a workaround since the Sqlite backend doesn't support `RETURNING`
    // Huge caveat with this is that it depends on whatever the last insert was, prevents concurrent inserts.
    fn last_insert_id(&mut self) -> Result<i32> {
        Ok(
            diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>(
                "last_insert_rowid()",
            ))
            .get_result(self.connection)?,
        )
    }

    /// Create or update an alias mapping between a local (client) message id and a server message id.
    pub fn set_message_alias(
        &mut self,
        local_id_in: &str,
        server_id_in: &str,
        conversation_id_in: &str,
    ) -> Result<()> {
        use crate::schema::message_aliases::dsl::*;
        diesel::replace_into(message_aliases)
            .values((
                local_id.eq(local_id_in),
                server_id.eq(server_id_in),
                conversation_id.eq(conversation_id_in),
            ))
            .execute(self.connection)?;
        Ok(())
    }

    /// Returns the local id for a given server id, if any.
    pub fn get_local_id_for(&mut self, server_id_in: &str) -> Result<Option<String>> {
        use crate::schema::message_aliases::dsl::*;
        let result = message_aliases
            .filter(server_id.eq(server_id_in))
            .select(local_id)
            .first::<String>(self.connection)
            .optional()?;
        Ok(result)
    }

    /// Batch lookup: returns a map server_id -> local_id for the provided server ids.
    pub fn get_local_ids_for(
        &mut self,
        server_ids_in: Vec<String>,
    ) -> Result<HashMap<String, String>> {
        use crate::schema::message_aliases::dsl::*;
        if server_ids_in.is_empty() {
            return Ok(HashMap::new());
        }

        let rows: Vec<(String, String)> = message_aliases
            .filter(server_id.eq_any(&server_ids_in))
            .select((server_id, local_id))
            .load::<(String, String)>(self.connection)?;

        let mut map = HashMap::new();
        for (sid, lid) in rows {
            map.insert(sid, lid);
        }
        Ok(map)
    }

    /// Update the contact_id for an existing participant record.
    pub fn update_participant_contact(
        &mut self,
        participant_handle: &str,
        new_contact_id: &str,
    ) -> Result<()> {
        use crate::schema::participants::dsl::*;

        log::debug!(target: target::REPOSITORY, "Updating participant contact {} => {}", participant_handle, new_contact_id);
        diesel::update(participants.filter(handle.eq(participant_handle)))
            .set(contact_id.eq(Some(new_contact_id.to_string())))
            .execute(self.connection)?;
        Ok(())
    }

    fn get_or_create_participant(&mut self, participant: &Participant) -> Option<String> {
        match participant {
            Participant::Me => None,
            Participant::Remote {
                handle: p_handle,
                contact_id: c_id,
                ..
            } => {
                use crate::schema::participants::dsl::*;

                let existing_participant = participants
                    .filter(handle.eq(p_handle))
                    .first::<ParticipantRecord>(self.connection)
                    .optional()
                    .unwrap();

                if existing_participant.is_none() {
                    let participant_record = InsertableParticipantRecord {
                        handle: p_handle.clone(),
                        is_me: false,
                        contact_id: c_id.clone(),
                    };

                    diesel::insert_into(participants)
                        .values(&participant_record)
                        .execute(self.connection)
                        .unwrap();
                }

                Some(p_handle.clone())
            }
        }
    }
}
