#![cfg(target_os = "linux")]

use crate::worker::{
    Attachment, AttachmentInfo, AttachmentMetadata, AttributionInfo, ChatMessage,
    ConversationSummary, DaemonClient, Event, SettingsSnapshot,
};
use anyhow::Result;
use dbus::arg::{cast, PropMap, RefArg};
use dbus::blocking::{Connection, Proxy};
use dbus::channel::Token;
use std::sync::mpsc::Sender;
use std::time::Duration;

const DBUS_NAME: &str = "net.buzzert.kordophonecd";
const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon";

#[allow(unused)]
mod dbus_interface {
    #![allow(unused)]
    include!(concat!(env!("OUT_DIR"), "/kordophone-client.rs"));
}
use dbus_interface::NetBuzzertKordophoneRepository as KordophoneRepository;
use dbus_interface::NetBuzzertKordophoneSettings as KordophoneSettings;

pub(crate) struct DBusClient {
    conn: Connection,
    signal_tokens: Vec<Token>,
}

impl DBusClient {
    pub(crate) fn new() -> Result<Self> {
        Ok(Self {
            conn: Connection::new_session()?,
            signal_tokens: Vec::new(),
        })
    }

    fn proxy(&self) -> Proxy<'_, &Connection> {
        self.conn
            .with_proxy(DBUS_NAME, DBUS_PATH, std::time::Duration::from_millis(5000))
    }
}

fn get_string(map: &PropMap, key: &str) -> String {
    map.get(key)
        .and_then(|v| v.0.as_str())
        .unwrap_or_default()
        .to_string()
}

fn get_i64(map: &PropMap, key: &str) -> i64 {
    map.get(key).and_then(|v| v.0.as_i64()).unwrap_or(0)
}

fn get_u32(map: &PropMap, key: &str) -> u32 {
    get_i64(map, key).try_into().unwrap_or(0)
}

fn get_bool(map: &PropMap, key: &str) -> bool {
    map.get(key).and_then(|v| v.0.as_i64()).unwrap_or(0) != 0
}

fn get_vec_string(map: &PropMap, key: &str) -> Vec<String> {
    map.get(key)
        .and_then(|v| v.0.as_iter())
        .map(|iter| {
            iter.filter_map(|item| item.as_str().map(|s| s.to_string()))
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn clone_prop_map(map: &PropMap) -> PropMap {
    map.iter()
        .map(|(key, value)| (key.clone(), dbus::arg::Variant(value.0.box_clone())))
        .collect()
}

fn get_prop_map(map: &PropMap, key: &str) -> Option<PropMap> {
    let value = map.get(key)?;

    if let Some(prop_map) = cast::<PropMap>(&value.0) {
        return Some(clone_prop_map(prop_map));
    }

    let mut iter = value.0.as_iter()?;
    let mut out = PropMap::new();
    loop {
        let Some(key) = iter.next().and_then(|arg| arg.as_str()) else {
            break;
        };
        let Some(value) = iter.next() else {
            break;
        };

        out.insert(key.to_string(), dbus::arg::Variant(value.box_clone()));
    }

    Some(out)
}

fn get_vec_prop_map(map: &PropMap, key: &str) -> Vec<PropMap> {
    let Some(value) = map.get(key) else {
        return Vec::new();
    };

    if let Some(items) = cast::<Vec<PropMap>>(&value.0) {
        return items.iter().map(clone_prop_map).collect();
    }

    value
        .0
        .as_iter()
        .map(|iter| {
            iter.filter_map(|item| {
                let mut item_iter = item.as_iter()?;
                let mut out = PropMap::new();
                loop {
                    let Some(key) = item_iter.next().and_then(|arg| arg.as_str()) else {
                        break;
                    };
                    let Some(value) = item_iter.next() else {
                        break;
                    };

                    out.insert(key.to_string(), dbus::arg::Variant(value.box_clone()));
                }
                Some(out)
            })
            .collect()
        })
        .unwrap_or_default()
}

fn attachment_from_prop_map(map: PropMap) -> Attachment {
    let metadata = get_prop_map(&map, "metadata").map(|metadata_map| {
        let attribution_info =
            get_prop_map(&metadata_map, "attribution_info").map(|info_map| AttributionInfo {
                width: info_map
                    .get("width")
                    .and_then(|v| v.0.as_i64())
                    .and_then(|v| v.try_into().ok()),
                height: info_map
                    .get("height")
                    .and_then(|v| v.0.as_i64())
                    .and_then(|v| v.try_into().ok()),
            });

        AttachmentMetadata { attribution_info }
    });

    Attachment {
        guid: get_string(&map, "guid"),
        downloaded: get_bool(&map, "downloaded"),
        preview_downloaded: get_bool(&map, "preview_downloaded"),
        metadata,
    }
}

fn password_for_username(username: &str) -> Result<String> {
    if username.trim().is_empty() {
        return Ok(String::new());
    }

    let entry = keyring::Entry::new("net.buzzert.kordophonecd", username)?;
    match entry.get_password() {
        Ok(password) => Ok(password),
        Err(keyring::Error::NoEntry) => Ok(String::new()),
        Err(e) => Err(e.into()),
    }
}

fn set_password_for_username(username: &str, password: &str) -> Result<()> {
    if username.trim().is_empty() {
        return Ok(());
    }

    let entry = keyring::Entry::new("net.buzzert.kordophonecd", username)?;
    if password.is_empty() {
        match entry.delete_credential() {
            Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
            Err(e) => Err(e.into()),
        }
    } else {
        entry.set_password(password).map_err(Into::into)
    }
}

impl DaemonClient for DBusClient {
    fn get_conversations(&mut self, limit: i32, offset: i32) -> Result<Vec<ConversationSummary>> {
        let mut items = KordophoneRepository::get_conversations(&self.proxy(), limit, offset)?;
        let mut conversations = items
            .drain(..)
            .map(|conv| {
                let id = get_string(&conv, "guid");
                let display_name = get_string(&conv, "display_name");
                let participants = get_vec_string(&conv, "participants");
                let title = if !display_name.trim().is_empty() {
                    display_name
                } else if participants.is_empty() {
                    "<unknown>".to_string()
                } else {
                    participants.join(", ")
                };

                ConversationSummary {
                    id,
                    title,
                    preview: get_string(&conv, "last_message_preview").replace('\n', " "),
                    participants,
                    unread_count: get_u32(&conv, "unread_count"),
                    date_unix: get_i64(&conv, "date"),
                }
            })
            .collect::<Vec<_>>();

        conversations.sort_by_key(|c| std::cmp::Reverse(c.date_unix));
        Ok(conversations)
    }

    fn get_messages(
        &mut self,
        conversation_id: String,
        last_message_id: Option<String>,
    ) -> Result<Vec<ChatMessage>> {
        let messages = KordophoneRepository::get_messages(
            &self.proxy(),
            &conversation_id,
            &last_message_id.unwrap_or_default(),
        )?;

        Ok(messages
            .into_iter()
            .map(|msg| ChatMessage {
                id: get_string(&msg, "id"),
                sender: get_string(&msg, "sender"),
                text: get_string(&msg, "text"),
                date_unix: get_i64(&msg, "date"),
                attachments: get_vec_prop_map(&msg, "attachments")
                    .into_iter()
                    .map(attachment_from_prop_map)
                    .collect(),
            })
            .collect())
    }

    fn send_message(&mut self, conversation_id: String, text: String) -> Result<Option<String>> {
        self.send_message_with_attachments(conversation_id, text, Vec::new())
    }

    fn send_message_with_attachments(
        &mut self,
        conversation_id: String,
        text: String,
        attachment_guids: Vec<String>,
    ) -> Result<Option<String>> {
        let attachment_guid_refs = attachment_guids
            .iter()
            .map(String::as_str)
            .collect::<Vec<_>>();
        let outgoing_id = KordophoneRepository::send_message(
            &self.proxy(),
            &conversation_id,
            &text,
            attachment_guid_refs,
        )?;
        Ok(Some(outgoing_id))
    }

    fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()> {
        KordophoneRepository::mark_conversation_as_read(&self.proxy(), &conversation_id)
            .map_err(|e| anyhow::anyhow!("Failed to mark conversation as read: {e}"))
    }

    fn sync_conversation(&mut self, conversation_id: String) -> Result<()> {
        KordophoneRepository::sync_conversation(&self.proxy(), &conversation_id)
            .map_err(|e| anyhow::anyhow!("Failed to sync conversation: {e}"))
    }

    fn sync_conversation_list(&mut self) -> Result<()> {
        KordophoneRepository::sync_conversation_list(&self.proxy())
            .map_err(|e| anyhow::anyhow!("Failed to sync conversation list: {e}"))
    }

    fn sync_all_conversations(&mut self) -> Result<()> {
        KordophoneRepository::sync_all_conversations(&self.proxy())
            .map_err(|e| anyhow::anyhow!("Failed to sync all conversations: {e}"))
    }

    fn download_attachment(&mut self, attachment_guid: String, preview: bool) -> Result<()> {
        KordophoneRepository::download_attachment(&self.proxy(), &attachment_guid, preview)
            .map_err(|e| anyhow::anyhow!("Failed to download attachment: {e}"))
    }

    fn get_attachment_info(&mut self, attachment_guid: String) -> Result<AttachmentInfo> {
        let (path, preview_path, downloaded, preview_downloaded) =
            KordophoneRepository::get_attachment_info(&self.proxy(), &attachment_guid)?;

        Ok(AttachmentInfo {
            path,
            preview_path,
            downloaded,
            preview_downloaded,
        })
    }

    fn upload_attachment(&mut self, path: String) -> Result<String> {
        KordophoneRepository::upload_attachment(&self.proxy(), &path)
            .map_err(|e| anyhow::anyhow!("Failed to upload attachment: {e}"))
    }

    fn get_settings(&mut self) -> Result<SettingsSnapshot> {
        let server_url = KordophoneSettings::server_url(&self.proxy()).unwrap_or_default();
        let username = KordophoneSettings::username(&self.proxy()).unwrap_or_default();
        let password = password_for_username(&username)?;

        Ok(SettingsSnapshot {
            server_url,
            username,
            password,
        })
    }

    fn save_settings(
        &mut self,
        server_url: String,
        username: String,
        password: String,
    ) -> Result<()> {
        KordophoneSettings::set_server(&self.proxy(), &server_url, &username)
            .map_err(|e| anyhow::anyhow!("Failed to save daemon settings: {e}"))?;
        set_password_for_username(&username, &password)
    }

    fn install_signal_handlers(&mut self, event_tx: Sender<Event>) -> Result<()> {
        let conversations_tx = event_tx.clone();
        let t1 = self
            .proxy()
            .match_signal(
                move |_: dbus_interface::NetBuzzertKordophoneRepositoryConversationsUpdated,
                      _: &Connection,
                      _: &dbus::message::Message| {
                    let _ = conversations_tx.send(Event::ConversationsUpdated);
                    true
                },
            )
            .map_err(|e| anyhow::anyhow!("Failed to match ConversationsUpdated: {e}"))?;

        let messages_tx = event_tx.clone();
        let t2 = self
            .proxy()
            .match_signal(
                move |s: dbus_interface::NetBuzzertKordophoneRepositoryMessagesUpdated,
                      _: &Connection,
                      _: &dbus::message::Message| {
                    let _ = messages_tx.send(Event::MessagesUpdated {
                        conversation_id: s.conversation_id,
                    });
                    true
                },
            )
            .map_err(|e| anyhow::anyhow!("Failed to match MessagesUpdated: {e}"))?;

        let reconnected_tx = event_tx.clone();
        let t3 = self
            .proxy()
            .match_signal(
                move |_: dbus_interface::NetBuzzertKordophoneRepositoryUpdateStreamReconnected,
                      _: &Connection,
                      _: &dbus::message::Message| {
                    let _ = reconnected_tx.send(Event::UpdateStreamReconnected);
                    true
                },
            )
            .map_err(|e| anyhow::anyhow!("Failed to match UpdateStreamReconnected: {e}"))?;

        let download_tx = event_tx.clone();
        let t4 = self
            .proxy()
            .match_signal(
                move |s: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentDownloadCompleted,
                      _: &Connection,
                      _: &dbus::message::Message| {
                    let _ = download_tx.send(Event::AttachmentDownloaded {
                        attachment_guid: s.attachment_id,
                    });
                    true
                },
            )
            .map_err(|e| anyhow::anyhow!("Failed to match AttachmentDownloadCompleted: {e}"))?;

        let failed_tx = event_tx.clone();
        let t5 = self
            .proxy()
            .match_signal(
                move |s: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentDownloadFailed,
                      _: &Connection,
                      _: &dbus::message::Message| {
                    let _ = failed_tx.send(Event::AttachmentDownloadFailed {
                        attachment_guid: s.attachment_id,
                        error: s.error_message,
                    });
                    true
                },
            )
            .map_err(|e| anyhow::anyhow!("Failed to match AttachmentDownloadFailed: {e}"))?;

        let upload_tx = event_tx;
        let t6 = self
            .proxy()
            .match_signal(
                move |s: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentUploadCompleted,
                      _: &Connection,
                      _: &dbus::message::Message| {
                    let _ = upload_tx.send(Event::AttachmentUploaded {
                        upload_guid: s.upload_guid,
                        attachment_guid: s.attachment_guid,
                    });
                    true
                },
            )
            .map_err(|e| anyhow::anyhow!("Failed to match AttachmentUploadCompleted: {e}"))?;

        self.signal_tokens.extend([t1, t2, t3, t4, t5, t6]);
        Ok(())
    }

    fn poll(&mut self, timeout: Duration) -> Result<()> {
        self.conn.process(timeout)?;
        Ok(())
    }
}
