use dbus::arg;
use dbus_tree::MethodErr;
use std::fs::OpenOptions;
use std::os::fd::{FromRawFd, IntoRawFd};
use std::sync::Arc;
use std::time::Instant;
use std::{future::Future, thread};
use tokio::sync::{mpsc, oneshot, Mutex};

use kordophoned::daemon::{
    contact_resolver::{ContactResolver, DefaultContactResolverBackend},
    events::{Event, Reply},
    settings::Settings,
    signals::Signal,
    DaemonResult,
};

use kordophone_db::models::participant::Participant;

use crate::dbus::endpoint::DbusRegistry;
use crate::dbus::interface;
use crate::dbus::interface::signals as DbusSignals;
use dbus_tokio::connection;

#[derive(Clone)]
pub struct DBusAgent {
    event_sink: mpsc::Sender<Event>,
    signal_receiver: Arc<Mutex<Option<mpsc::Receiver<Signal>>>>,
    contact_resolver: ContactResolver<DefaultContactResolverBackend>,
}

impl DBusAgent {
    pub fn new(event_sink: mpsc::Sender<Event>, signal_receiver: mpsc::Receiver<Signal>) -> Self {
        Self {
            event_sink,
            signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))),
            contact_resolver: ContactResolver::new(DefaultContactResolverBackend::default()),
        }
    }

    pub async fn run(self) {
        // Establish a session bus connection.
        let (resource, connection) =
            connection::new_session_sync().expect("Failed to connect to session bus");

        // Ensure the D-Bus resource is polled.
        tokio::spawn(async move {
            let err = resource.await;
            panic!("Lost connection to D-Bus: {:?}", err);
        });

        // Claim well-known bus name.
        connection
            .request_name(interface::NAME, false, true, false)
            .await
            .expect("Unable to acquire D-Bus name");

        // Registry for objects & signals.
        let dbus_registry = DbusRegistry::new(connection.clone());

        // Register our object implementation.
        let implementation = self.clone();
        dbus_registry.register_object(interface::OBJECT_PATH, implementation, |cr| {
            vec![
                interface::register_net_buzzert_kordophone_repository(cr),
                interface::register_net_buzzert_kordophone_settings(cr),
            ]
        });

        // Spawn task that forwards daemon signals to D-Bus.
        {
            let registry = dbus_registry.clone();
            let receiver_arc = self.signal_receiver.clone();
            tokio::spawn(async move {
                let mut receiver = receiver_arc
                    .lock()
                    .await
                    .take()
                    .expect("Signal receiver already taken");

                while let Some(signal) = receiver.recv().await {
                    match signal {
                        Signal::ConversationsUpdated => {
                            log::debug!("Sending signal: ConversationsUpdated");
                            registry
                                .send_signal(
                                    interface::OBJECT_PATH,
                                    DbusSignals::ConversationsUpdated {},
                                )
                                .unwrap_or_else(|_| {
                                    log::error!("Failed to send signal");
                                    0
                                });
                        }
                        Signal::MessagesUpdated(conversation_id) => {
                            log::debug!(
                                "Sending signal: MessagesUpdated for conversation {}",
                                conversation_id
                            );
                            registry
                                .send_signal(
                                    interface::OBJECT_PATH,
                                    DbusSignals::MessagesUpdated { conversation_id },
                                )
                                .unwrap_or_else(|_| {
                                    log::error!("Failed to send signal");
                                    0
                                });
                        }
                        Signal::AttachmentDownloaded(attachment_id) => {
                            log::debug!(
                                "Sending signal: AttachmentDownloaded for attachment {}",
                                attachment_id
                            );
                            registry
                                .send_signal(
                                    interface::OBJECT_PATH,
                                    DbusSignals::AttachmentDownloadCompleted { attachment_id },
                                )
                                .unwrap_or_else(|_| {
                                    log::error!("Failed to send signal");
                                    0
                                });
                        }
                        Signal::AttachmentUploaded(upload_guid, attachment_guid) => {
                            log::debug!(
                                "Sending signal: AttachmentUploaded for upload {}, attachment {}",
                                upload_guid,
                                attachment_guid
                            );
                            registry
                                .send_signal(
                                    interface::OBJECT_PATH,
                                    DbusSignals::AttachmentUploadCompleted {
                                        upload_guid,
                                        attachment_guid,
                                    },
                                )
                                .unwrap_or_else(|_| {
                                    log::error!("Failed to send signal");
                                    0
                                });
                        }
                        Signal::UpdateStreamReconnected => {
                            log::debug!("Sending signal: UpdateStreamReconnected");
                            registry
                                .send_signal(
                                    interface::OBJECT_PATH,
                                    DbusSignals::UpdateStreamReconnected {},
                                )
                                .unwrap_or_else(|_| {
                                    log::error!("Failed to send signal");
                                    0
                                });
                        }
                    }
                }
            });
        }

        // Keep running forever.
        std::future::pending::<()>().await;
    }

    pub async fn send_event<T>(
        &self,
        make_event: impl FnOnce(Reply<T>) -> Event,
    ) -> DaemonResult<T> {
        let (reply_tx, reply_rx) = oneshot::channel();
        self.event_sink
            .send(make_event(reply_tx))
            .await
            .map_err(|_| "Failed to send event")?;

        reply_rx.await.map_err(|_| "Failed to receive reply".into())
    }

    pub fn send_event_sync<T: Send>(
        &self,
        make_event: impl FnOnce(Reply<T>) -> Event + Send,
    ) -> Result<T, MethodErr> {
        let daemon_result = run_sync_future(self.send_event(make_event))?;
        daemon_result.map_err(|e| MethodErr::failed(&format!("Daemon error: {}", e)))
    }

    fn resolve_participant_display_name(&mut self, participant: &Participant) -> String {
        match participant {
            // Me (we should use a special string here...)
            Participant::Me => "(Me)".to_string(),

            // Remote participant with a resolved contact_id
            Participant::Remote {
                handle,
                contact_id: Some(contact_id),
                ..
            } => self
                .contact_resolver
                .get_contact_display_name(contact_id)
                .unwrap_or_else(|| handle.clone()),

            // Remote participant without a resolved contact_id
            Participant::Remote { handle, .. } => handle.clone(),
        }
    }
}

//
// D-Bus repository interface implementation
//

use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository;
use crate::dbus::interface::NetBuzzertKordophoneSettings as DbusSettings;

impl DbusRepository for DBusAgent {
    fn get_version(&mut self) -> Result<String, MethodErr> {
        self.send_event_sync(Event::GetVersion)
    }

    fn get_conversations(
        &mut self,
        limit: i32,
        offset: i32,
    ) -> Result<Vec<arg::PropMap>, MethodErr> {
        self.send_event_sync(|r| Event::GetAllConversations(limit, offset, r))
            .map(|conversations| {
                conversations
                    .into_iter()
                    .map(|conv| {
                        let mut map = arg::PropMap::new();
                        map.insert("guid".into(), arg::Variant(Box::new(conv.guid)));
                        map.insert(
                            "display_name".into(),
                            arg::Variant(Box::new(conv.display_name.unwrap_or_default())),
                        );
                        map.insert(
                            "unread_count".into(),
                            arg::Variant(Box::new(conv.unread_count as i32)),
                        );
                        map.insert(
                            "last_message_preview".into(),
                            arg::Variant(Box::new(conv.last_message_preview.unwrap_or_default())),
                        );
                        map.insert(
                            "participants".into(),
                            arg::Variant(Box::new(
                                conv.participants
                                    .into_iter()
                                    .map(|p| self.resolve_participant_display_name(&p))
                                    .collect::<Vec<String>>(),
                            )),
                        );
                        map.insert(
                            "date".into(),
                            arg::Variant(Box::new(conv.date.and_utc().timestamp())),
                        );
                        map
                    })
                    .collect()
            })
    }

    fn sync_conversation_list(&mut self) -> Result<(), MethodErr> {
        self.send_event_sync(Event::SyncConversationList)
    }

    fn sync_all_conversations(&mut self) -> Result<(), MethodErr> {
        self.send_event_sync(Event::SyncAllConversations)
    }

    fn sync_conversation(&mut self, conversation_id: String) -> Result<(), MethodErr> {
        self.send_event_sync(|r| Event::SyncConversation(conversation_id, r))
    }

    fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<(), MethodErr> {
        self.send_event_sync(|r| Event::MarkConversationAsRead(conversation_id, r))
    }

    fn get_messages(
        &mut self,
        conversation_id: String,
        last_message_id: String,
    ) -> Result<Vec<arg::PropMap>, MethodErr> {
        let started = Instant::now();
        let last_message_id_opt = if last_message_id.is_empty() {
            None
        } else {
            Some(last_message_id)
        };

        let messages =
            self.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r))?;

        let mut attachment_count: usize = 0;
        let mut text_bytes: usize = 0;

        let mapped: Vec<arg::PropMap> = messages
            .into_iter()
            .map(|msg| {
                let mut map = arg::PropMap::new();
                map.insert("id".into(), arg::Variant(Box::new(msg.id)));

                // Remove the attachment placeholder here.
                let text = msg.text.replace("\u{FFFC}", "");
                text_bytes += text.len();

                map.insert("text".into(), arg::Variant(Box::new(text)));
                map.insert(
                    "date".into(),
                    arg::Variant(Box::new(msg.date.and_utc().timestamp())),
                );
                map.insert(
                    "sender".into(),
                    arg::Variant(Box::new(msg.sender.display_name())),
                );

                if !msg.attachments.is_empty() {
                    let attachments: Vec<arg::PropMap> = msg
                        .attachments
                        .into_iter()
                        .map(|attachment| {
                        attachment_count += 1;
                        let mut attachment_map = arg::PropMap::new();
                        attachment_map.insert(
                            "guid".into(),
                            arg::Variant(Box::new(attachment.guid.clone())),
                        );
                        attachment_map.insert(
                            "downloaded".into(),
                            arg::Variant(Box::new(attachment.is_downloaded(false))),
                        );
                        attachment_map.insert(
                            "preview_downloaded".into(),
                            arg::Variant(Box::new(attachment.is_downloaded(true))),
                        );

                        if let Some(ref metadata) = attachment.metadata {
                            let mut metadata_map = arg::PropMap::new();

                            if let Some(ref attribution_info) = metadata.attribution_info {
                                let mut attribution_map = arg::PropMap::new();
                                if let Some(width) = attribution_info.width {
                                    attribution_map.insert(
                                        "width".into(),
                                        arg::Variant(Box::new(width as i32)),
                                    );
                                }
                                if let Some(height) = attribution_info.height {
                                    attribution_map.insert(
                                        "height".into(),
                                        arg::Variant(Box::new(height as i32)),
                                    );
                                }
                                metadata_map.insert(
                                    "attribution_info".into(),
                                    arg::Variant(Box::new(attribution_map)),
                                );
                            }

                            attachment_map.insert(
                                "metadata".into(),
                                arg::Variant(Box::new(metadata_map)),
                            );
                        }
                            attachment_map
                        })
                        .collect();

                    map.insert("attachments".into(), arg::Variant(Box::new(attachments)));
                }

                map
            })
            .collect();

        log::debug!(
            target: "dbus",
            "GetMessages mapped in {}ms: {} messages, {} attachments, {} text-bytes",
            started.elapsed().as_millis(),
            mapped.len(),
            attachment_count,
            text_bytes
        );

        Ok(mapped)
    }

    fn delete_all_conversations(&mut self) -> Result<(), MethodErr> {
        self.send_event_sync(Event::DeleteAllConversations)
    }

    fn send_message(
        &mut self,
        conversation_id: String,
        text: String,
        attachment_guids: Vec<String>,
    ) -> Result<String, MethodErr> {
        self.send_event_sync(|r| Event::SendMessage(conversation_id, text, attachment_guids, r))
            .map(|uuid| uuid.to_string())
    }

    fn get_attachment_info(
        &mut self,
        attachment_id: String,
    ) -> Result<(String, String, bool, bool), MethodErr> {
        self.send_event_sync(|r| Event::GetAttachment(attachment_id, r))
            .map(|attachment| {
                let path = attachment.get_path_for_preview(false);
                let downloaded = attachment.is_downloaded(false);
                let preview_path = attachment.get_path_for_preview(true);
                let preview_downloaded = attachment.is_downloaded(true);
                (
                    path.to_string_lossy().to_string(),
                    preview_path.to_string_lossy().to_string(),
                    downloaded,
                    preview_downloaded,
                )
            })
    }

    fn download_attachment(
        &mut self,
        attachment_id: String,
        preview: bool,
    ) -> Result<(), MethodErr> {
        self.send_event_sync(|r| Event::DownloadAttachment(attachment_id, preview, r))
    }

    fn open_attachment_fd(
        &mut self,
        attachment_id: String,
        preview: bool,
    ) -> Result<arg::OwnedFd, MethodErr> {
        let attachment = self.send_event_sync(|r| Event::GetAttachment(attachment_id, r))?;
        let path = attachment.get_path_for_preview(preview);

        let file = OpenOptions::new()
            .read(true)
            .open(&path)
            .map_err(|e| MethodErr::failed(&format!("Failed to open attachment: {}", e)))?;

        let fd = file.into_raw_fd();
        Ok(unsafe { arg::OwnedFd::from_raw_fd(fd) })
    }

    fn upload_attachment(&mut self, path: String) -> Result<String, MethodErr> {
        use std::path::PathBuf;
        let path = PathBuf::from(path);
        self.send_event_sync(|r| Event::UploadAttachment(path, r))
    }
}

//
// D-Bus settings interface implementation.
//

impl DbusSettings for DBusAgent {
    fn set_server(&mut self, url: String, user: String) -> Result<(), MethodErr> {
        self.send_event_sync(|r| {
            Event::UpdateSettings(
                Settings {
                    server_url: Some(url),
                    username: Some(user),
                    token: None,
                },
                r,
            )
        })
    }

    fn server_url(&self) -> Result<String, MethodErr> {
        self.send_event_sync(Event::GetAllSettings)
            .map(|settings| settings.server_url.unwrap_or_default())
    }

    fn set_server_url(&self, value: String) -> Result<(), MethodErr> {
        self.send_event_sync(|r| {
            Event::UpdateSettings(
                Settings {
                    server_url: Some(value),
                    username: None,
                    token: None,
                },
                r,
            )
        })
    }

    fn username(&self) -> Result<String, MethodErr> {
        self.send_event_sync(Event::GetAllSettings)
            .map(|settings| settings.username.unwrap_or_default())
    }

    fn set_username(&self, value: String) -> Result<(), MethodErr> {
        self.send_event_sync(|r| {
            Event::UpdateSettings(
                Settings {
                    server_url: None,
                    username: Some(value),
                    token: None,
                },
                r,
            )
        })
    }
}

//
// Helper utilities.
//

fn run_sync_future<F, T>(f: F) -> Result<T, MethodErr>
where
    T: Send,
    F: Future<Output = T> + Send,
{
    let joined = thread::scope(move |s| {
        s.spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .map_err(|_| MethodErr::failed("Unable to create tokio runtime"))?;

            let result = rt.block_on(f);
            Ok(result)
        })
        .join()
    });

    match joined {
        Ok(result) => result,
        Err(_) => Err(MethodErr::failed("Error joining runtime thread")),
    }
}
