use std::{
    collections::{HashMap, HashSet},
    sync::{Arc, Mutex},
};

use chrono::Utc;
use ftypes::{
    Error, ErrorKind, Message, MessageId, MetaMessageContent, ReceivedMessage,
    ReceivedMessageContent, Result, Store, ThreadId,
    convert::{
        from_presage::MessageConversionResult, to_presage::MessageConversionAdditionalDataProvider,
    },
};
use futures::{FutureExt, StreamExt, select};
use libsignal_service::prelude::Content;
use presage::{manager::Registered, store::ContentsStore};
use tokio::sync::RwLock;

use crate::{Function, LinkSetup, ManagerHandle, RegisterSetup, SetupDecision, SignalClient};

const ERROR_TIMEOUT: u64 = 15;

pub struct Manager<S: Store, C: SignalClient<S>> {
    store: S,
    client: Arc<C>,
    pending_messages: Arc<Mutex<HashMap<MessageId, Vec<Content>>>>,
}

impl<S: Store + Clone, C: SignalClient<S>> Clone for Manager<S, C> {
    fn clone(&self) -> Self {
        Self {
            store: self.store.clone(),
            client: self.client.clone(),
            pending_messages: self.pending_messages.clone(),
        }
    }
}

impl<S: Clone + Store + 'static, C: SignalClient<S> + 'static> Manager<S, C> {
    pub fn new(store: S, client: C) -> Self {
        Self {
            store,
            client: Arc::new(client),
            pending_messages: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    async fn setup_manager(
        &self,
    ) -> std::result::Result<
        presage::Manager<S::PresageStore, Registered>,
        presage::Error<<S::PresageStore as presage::store::Store>::Error>,
    > {
        let presage_store = self.store.presage_store();
        return if let Ok(manager) = presage::Manager::load_registered(presage_store.clone()).await {
            Ok(manager)
        } else {
            // Manager is not yet registered; setup is pending.
            match self.client.request_setup_decision().await {
                SetupDecision::Link(LinkSetup {
                    servers,
                    device_name,
                }) => {
                    let (tx_link, rx_link) = futures::channel::oneshot::channel();
                    let (_, manager) = futures::future::join(
                        async {
                            let link = rx_link.await.expect("Failed to receive link callback");
                            self.client.request_qrcode_scan(link).await;
                        },
                        presage::Manager::link_secondary_device(
                            presage_store.clone(),
                            servers,
                            device_name,
                            tx_link,
                        ),
                    )
                    .await;
                    manager
                }
                SetupDecision::Register(RegisterSetup {
                    servers,
                    phone_number,
                    captcha,
                }) => {
                    let manager = presage::Manager::register(
                        presage_store,
                        presage::manager::RegistrationOptions {
                            signal_servers: servers,
                            phone_number,
                            use_voice_call: false,
                            captcha: Some(&captcha[..]),
                            force: false,
                        },
                    )
                    .await;
                    match manager {
                        Ok(manager) => {
                            let confirmation = self.client.request_confirmation_code().await;
                            manager.confirm_verification_code(&confirmation).await
                        }
                        Err(e) => Err(e),
                    }
                }
            }
        };
    }

    async fn handle_content(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        content: Content,
    ) {
        let messages =
            ftypes::convert::from_presage::message(content.clone(), self.store.clone()).await;
        match messages {
            Ok(MessageConversionResult::Messages(messages)) if messages.is_empty() => {
                log::warn!(
                    "Got content from presage that cannot be parsed: {:#?}",
                    content
                );
            }
            Ok(MessageConversionResult::Messages(messages)) => {
                for m in messages {
                    self.handle_message(manager.clone(), m).await;
                }
            }
            Ok(MessageConversionResult::WaitingFor(message)) => {
                log::trace!("Message received out-of-order, waiting for {message:?}");
                self.pending_messages
                    .lock()
                    .expect("Poisoned mutex")
                    .entry(message)
                    .and_modify(|e| e.push(content.clone()))
                    .or_insert(vec![content]);
            }
            Err(e) => {
                self.client.error(e).await;
            }
        }
    }

    async fn handle_out_of_order_messages(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        message_id: MessageId,
    ) {
        let pending = self
            .pending_messages
            .lock()
            .expect("Poisoned mutex")
            .remove(&message_id)
            .unwrap_or_default();
        for content in pending {
            log::trace!("Re-handle out-of-order content for {message_id:?}");
            let manager = manager.clone();
            // Due to recursion with handle_content, need to box the future.
            Box::pin(async { self.handle_content(manager, content).await }).await
        }
    }

    async fn handle_message_fallible(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        message: ReceivedMessage,
    ) -> Result<()> {
        let mut auxiliary_messages = vec![];

        if let ReceivedMessageContent::Meta(MetaMessageContent::ContactChange(contact_change)) =
            &message.content
            && let Some(profile_key) = contact_change.secrets.profile_key
        {
            // For contact changes: Retrieve the updated profile; retrieve the locally stored contact; merge them; store the result.
            match manager
                .write()
                .await
                .retrieve_profile_by_uuid(
                    contact_change.contact.account_id.0.raw_uuid(),
                    profile_key,
                )
                .await
            {
                Ok(profile) => {
                    if let Some(presage_contact) = self
                        .store
                        .presage_store()
                        .contact_by_id(&contact_change.contact.account_id.0)
                        .await
                        .map_err(|e| {
                            ErrorKind::Database
                                .caused_by(e)
                                .with_message("failed to get contact by id due to contact change")
                                .add_message_context(message.clone())
                                .add_value_dbg_context(contact_change.contact.account_id.0)
                        })?
                    {
                        let contact = ftypes::convert::from_presage::contact(
                            presage_contact,
                            Some(profile_key),
                            Some(profile),
                            None,
                            None,
                        )?;
                        self.store.upsert_contact(contact).await?;
                    }
                }
                Err(e) => {
                    return Err(ftypes::Error::from(e)
                        .with_message("failed to update profile for contact")
                        .add_message_context(message));
                }
            }
        }

        if let ReceivedMessageContent::Meta(MetaMessageContent::GroupChange(group_change)) =
            &message.content
        {
            // For group changes: Retrieve the group stored by presage; convert it to a ftypes group; store the result.
            if let Some(presage_group) = self
                .store
                .presage_store()
                .group(group_change.secrets.master_key)
                .await
                .map_err(|e| {
                    ErrorKind::Database
                        .caused_by(e)
                        .with_message("failed to get group due to contact change")
                        .add_message_context(message.clone())
                        .add_value_dbg_context(group_change.secrets.master_key)
                })?
            {
                let group = ftypes::convert::from_presage::group(
                    group_change.secrets.master_key,
                    presage_group,
                );
                self.store.upsert_group(group.clone()).await?;

                // Group changes are also done when a contact in a group changes.
                // Threfore, also notify contact changes.
                // Cannot do that when converting the presage message to the flare-backend message, as at that point the new group is not yet available, it is fetched here.
                // (Well, in theory it is already in the store, but that is an implementation detail I don't want to depend on.)
                let mut member_message_id = message.id;
                member_message_id.get_and_next();
                auxiliary_messages.extend(group.members.into_iter().map(|m| Message {
                    id: member_message_id.get_and_next(),
                    thread: message.thread,
                    metadata: message.metadata,
                    content: ReceivedMessageContent::Meta(MetaMessageContent::ContactChange(
                        ftypes::ContactChangeMessage {
                            contact: m.id,
                            secrets: m.secrets,
                        },
                    )),
                }));
            }
        }

        self.store.store_message(message.clone()).await?;

        let message_id = message.id;

        log::trace!("Notify client of message");
        let client = self.client.clone();
        tokio::task::spawn_local(async move {
            client.on_message(message).await;
            for m in auxiliary_messages {
                client.on_message(m).await;
            }
        });

        self.handle_out_of_order_messages(manager.clone(), message_id)
            .await;

        Ok(())
    }

    async fn handle_message(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        message: ReceivedMessage,
    ) {
        if let Err(e) = self.handle_message_fallible(manager, message).await {
            self.client.error(e).await;
        }
    }

    async fn send_message(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        thread: ThreadId,
        message: Message<ReceivedMessageContent>,
    ) -> Result<()> {
        let message_id = message.id;
        let data_provider: DataProvider<S> = DataProvider(manager.clone());

        match thread {
            ThreadId::Contact(account_id) => {
                let presage_message =
                    ftypes::convert::to_presage::message(&message.content, None, &data_provider)
                        .await?;
                manager
                    .write()
                    .await
                    .send_message(
                        account_id.0,
                        presage_message,
                        message_id.timestamp.timestamp_millis() as u64,
                    )
                    .await
                    .map_err(|e| {
                        Error::from(e)
                            .with_message("failed to send the message to a contact")
                            .add_message_context(message.clone())
                            .add_value_dbg_context(account_id)
                    })?;
            }
            ThreadId::Group(group_id) => {
                let group = self.store.group(group_id).await;
                match group {
                    Ok(Some(group)) => {
                        let presage_message = ftypes::convert::to_presage::message(
                            &message.content,
                            Some((group_id, group.secrets)),
                            &data_provider,
                        )
                        .await?;
                        let group_master_key = group.secrets.master_key;
                        manager
                            .write()
                            .await
                            .send_message_to_group(
                                &group_master_key,
                                presage_message,
                                message_id.timestamp.timestamp_millis() as u64,
                            )
                            .await
                            .map_err(|e| {
                                Error::from(e)
                                    .with_message("failed to send the message to a group")
                                    .add_message_context(message.clone())
                                    .add_value_dbg_context(group_id)
                            })?
                    }
                    Ok(None) => {
                        Err(ErrorKind::Database
                            .error()
                            .with_message("group with the given id not found")
                            .add_message_context(message.clone()))?;
                    }
                    Err(e) => {
                        Err(e.add_message_context(message.clone()))?;
                    }
                }
            }
        }

        self.handle_message(manager, message).await;

        Ok(())
    }

    async fn handle_function_fallible(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        content: Function,
    ) -> Result<()> {
        match content {
            Function::SendMessage(thread_id, message) => {
                self.send_message(manager, thread_id, message).await
            }
            Function::ContactAvatar(contact_id, contact_secrets, sender) => {
                log::trace!("Got contact avatar request");
                let result = if let Some(profile_key) = contact_secrets.profile_key {
                    manager
                        .write()
                        .await
                        .retrieve_profile_avatar_by_uuid(
                            contact_id.account_id.0.raw_uuid(),
                            profile_key,
                        )
                        .await
                        .map_err(|e| {
                            Error::from(e)
                                .with_message("failed to query contact avatar")
                                .add_contact_context(contact_id)
                        })
                } else {
                    Ok(None)
                };
                log::trace!("Send back contact avatar response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");
                Ok(())
            }
            Function::GroupAvatar(group_id, group_secrets, sender) => {
                log::trace!("Got group avatar request");
                let result = manager
                    .write()
                    .await
                    .retrieve_group_avatar(presage::proto::GroupContextV2 {
                        master_key: Some(group_secrets.master_key.to_vec()),
                        revision: Some(group_secrets.revision),
                        group_change: None,
                    })
                    .await
                    .map_err(|e| {
                        Error::from(e)
                            .with_message("failed to query group avatar")
                            .add_group_context(group_id)
                    });
                log::trace!("Send back group avatar response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");
                Ok(())
            }
            Function::Attachment(attachment, sender) => {
                log::trace!("Got attachment request");
                let result = manager
                    .read()
                    .await
                    .get_attachment(&ftypes::convert::to_presage::attachment_pointer(attachment))
                    .await
                    .map_err(|e| Error::from(e).with_message("failed to query attachment"));
                log::trace!("Send back attachment response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");
                Ok(())
            }
            Function::RequestContacts => manager
                .write()
                .await
                .request_contacts()
                .await
                .map_err(|e| Error::from(e).with_message("failed to request contacts sync")),
            Function::LinkedDevices(sender) => {
                log::trace!("Got linked devices request");
                let result = manager
                    .read()
                    .await
                    .devices()
                    .await
                    .map_err(|e| Error::from(e).with_message("failed to query linked devices"))
                    .map(|d| {
                        d.into_iter()
                            .map(ftypes::convert::from_presage::device_info)
                            .collect()
                    });
                log::trace!("Send back linked devices response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");
                Ok(())
            }
            Function::LinkSecondary(url, sender) => {
                log::trace!("Got link-secondary request");
                let result = manager
                    .write()
                    .await
                    .link_secondary(url)
                    .await
                    .map_err(|e| Error::from(e).with_message("failed to link secondary device"));
                log::trace!("Send back link-secondary response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");
                Ok(())
            }
            Function::UnlinkSecondary(id, sender) => {
                log::trace!("Got unlink-secondary request");
                let result = manager
                    .write()
                    .await
                    .unlink_secondary(id)
                    .await
                    .map_err(|e| Error::from(e).with_message("failed to unlink secondary device"));
                log::trace!("Send back unlink-secondary response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");
                Ok(())
            }
            Function::SubmitCaptcha(captcha_token, url, sender) => {
                log::trace!("Got request to fill captcha");
                // Split out the interesting parts from the URL.
                let result = if let Some((_, c)) = url.to_string().split_once(".challenge.") {
                    manager
                        .read()
                        .await
                        .submit_recaptcha_challenge(&captcha_token.0, c)
                        .await
                        .map_err(|e| Error::from(e).with_message("failed to submit captcha"))
                } else {
                    Err(ErrorKind::Protocol
                        .error()
                        .with_message("given captcha url has invalid format")
                        .add_value_context(&url))
                };
                log::trace!("Send back captcha response");
                sender
                    .send(result)
                    .expect("failed to send back result from manager");

                Ok(())
            }
            Function::SendSessionReset(account_id, date_time) => {
                log::trace!("Got session reset request");
                manager
                    .write()
                    .await
                    .send_session_reset(&account_id.0, date_time.timestamp_millis() as u64)
                    .await?;
                Ok(())
            }
            Function::RestartReceive | Function::ShutdownReceive(_) => {
                unreachable!()
            }
        }
    }

    async fn handle_function(
        &self,
        manager: Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>,
        content: Function,
    ) {
        if let Err(e) = self.handle_function_fallible(manager, content).await {
            self.client.error(e).await;
        }
    }

    pub async fn receive(self) -> Result<()> {
        // Try loading from a registered store
        let manager = self.setup_manager().await?;
        let manager = Arc::new(RwLock::new(manager));

        // TODO: Consider a bounded channel.
        // This will then need to ensure not to fill out the buffer between setup-complete and queue-empty.
        let (sender, mut receiver) = futures::channel::mpsc::unbounded();
        let handle = ManagerHandle::new(sender, self.store.clone()).await?;

        // Cannot just await the future; the client could use the handle
        // during `setup_complete` which would lead to a deadlock as we
        // are not yet at the point of receiving requests from the handle.
        let client = self.client.clone();
        let mut setup_complete_handle = Some(tokio::task::spawn_local(async move {
            log::trace!("Start setup-complete");
            client.setup_complete(handle).await;
            log::trace!("Stop setup-complete")
        }));

        'outer: loop {
            let messages = manager
                .write()
                .await
                .receive_messages()
                .await
                .map_err(|e| e.into());

            let mut requires_session_reset = HashSet::new();

            match messages {
                Ok(mut messages) => {
                    // Receive until queue empty.

                    let mut next_msg = messages.next().fuse();

                    log::trace!("Manager awaiting for queue empty");
                    loop {
                        let msg = next_msg.await;
                        if let Some(msg) = msg {
                            match msg {
                                presage::model::messages::Received::QueueEmpty => {
                                    log::info!("Queue empty");
                                    break;
                                }
                                presage::model::messages::Received::Contacts => {
                                    // TODO: Handle?
                                }
                                presage::model::messages::Received::DecryptionError(recipient) => {
                                    log::warn!(
                                        "Received a decryption error; enqueueing session reset"
                                    );
                                    requires_session_reset.insert(recipient);
                                }
                                presage::model::messages::Received::Content(content) => {
                                    log::trace!("Manager received content from presage");
                                    self.handle_content(manager.clone(), *content).await
                                }
                            }
                        } else {
                            continue 'outer;
                        }
                        next_msg = messages.next().fuse();
                    }

                    if !requires_session_reset.is_empty() {
                        log::trace!("Handling pending requires session reset");
                        let man = manager.clone();
                        tokio::task::spawn_local(async move {
                            for recipient in requires_session_reset {
                                if let Err(e) = man
                                    .write()
                                    .await
                                    .send_session_reset(
                                        &recipient,
                                        Utc::now().timestamp_millis() as u64,
                                    )
                                    .await
                                {
                                    log::error!("Got an error trying to reset the session: {e}");
                                }
                            }
                        });
                    }

                    // Cannot just await the future; the client could use the handle
                    // during `setup_complete` which would lead to a deadlock as we
                    // are not yet at the point of receiving requests from the handle.
                    // Also need to ensure that the setup-complete future completes
                    // before sending the queue-empty signal.
                    let client = self.client.clone();
                    let handle = setup_complete_handle.take();
                    tokio::task::spawn_local(async move {
                        log::trace!("Start queue-empty");
                        if let Some(handle) = handle {
                            log::trace!("Need to finish setup-complete");
                            handle.await.expect("Failed to join setup complete handle");
                        };
                        client.queue_empty().await;
                        log::trace!("Finish queue-empty");
                    });

                    log::trace!("Manager awaiting for content, function, or client stop");
                    let mut next_msg = messages.next().fuse();
                    loop {
                        select! {
                            // Receiveing a message
                            msg = next_msg => {
                                log::trace!("Manager received content from presage");
                                if let Some(content) = msg {
                                    // TODO: Maybe also handle others?
                                    if let presage::model::messages::Received::Content(content) = content {
                                        self.handle_content(manager.clone(), *content).await;
                                        next_msg = messages.next().fuse();
                                    } else if let presage::model::messages::Received::DecryptionError(recipient) = content {
                                        log::warn!("Content was a decryption error; immediately sending session reset");
                                        let man = manager.clone();
                                        tokio::task::spawn_local(async move {
                                            if let Err(e) = man.write().await.send_session_reset(&recipient, Utc::now().timestamp_millis() as u64).await {
                                                log::error!("Got an error trying to reset the session: {e}");
                                            }
                                        });

                                    }
                                } else {
                                    break;
                                }

                            },
                            // Receive a function to execute.
                            func = receiver.next() => {
                                log::trace!("Manager received function it needs to handle");
                                if let Some(func) = func {
                                    if matches!(func, Function::RestartReceive) {
                                        log::trace!("Restarting the receive loop");
                                        break;
                                    }
                                    if let Function::ShutdownReceive(shutdown_sender) = func {
                                        log::trace!("Shutting down the receive loop");
                                        shutdown_sender.send(()).expect("Failed to send shutdown result");
                                        break 'outer;
                                    }

                                    let s = self.clone();
                                    let man = manager.clone();
                                    tokio::task::spawn_local(async move { s.handle_function(man, func).await });
                                }
                            },
                            complete => {
                                break;
                            }
                        }
                    }
                }
                Err(e) => {
                    self.client.error(e).await;
                    tokio::time::sleep(std::time::Duration::from_secs(ERROR_TIMEOUT)).await;
                }
            }
        }
        Ok(())
    }
}

pub struct DataProvider<S: Store>(Arc<RwLock<presage::Manager<S::PresageStore, Registered>>>);

impl<S: Store> MessageConversionAdditionalDataProvider for DataProvider<S> {
    async fn upload_attachment(
        &self,
        attachment: ftypes::Attachment,
    ) -> Result<ftypes::AttachmentPointer> {
        // TODO: Make content type and size non-optional?
        log::trace!("Uploading an attachment");
        let result = self
            .0
            .write()
            .await
            .upload_attachment(
                libsignal_service::sender::AttachmentSpec {
                    content_type: attachment
                        .properties
                        .content_type
                        .expect("Attachment pointer to have a content type"),
                    length: attachment
                        .properties
                        .size
                        .expect("Attachment pointer to have a size")
                        as usize,
                    file_name: attachment.properties.file_name,
                    preview: attachment.properties.thumbnail,
                    // TODO
                    voice_note: None,
                    borderless: None,
                    width: attachment.properties.width,
                    height: attachment.properties.height,
                    caption: attachment.properties.caption,
                    blur_hash: attachment.properties.blur_hash,
                },
                attachment.content,
            )
            .await
            .map_err(|e| Error::from(e).with_message("failed to upload attachment"))?
            .map_err(|e| Error::from(e).with_message("failed to upload attachment"))?;
        log::trace!("Attachment upload finished");
        ftypes::convert::from_presage::attachment_pointer(result)
    }
}
