use gettextrs::gettext;
use gtk::{glib, prelude::*, subclass::prelude::*};
use zbus::{
    names::{BusName, UniqueName, WellKnownName},
    zvariant,
};

use crate::{
    ConnectionType, message_tag::MessageTag, message_type::MessageType, monitor::Event,
    timestamp::Timestamp,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ReceiveIndex(u32);

mod imp {
    use std::cell::{Cell, OnceCell};

    use super::*;

    #[derive(Debug, Default, glib::Properties)]
    #[properties(wrapper_type = super::Message)]
    pub struct Message {
        #[property(get, set, construct_only, default)]
        pub(super) message_type: Cell<MessageType>,
        #[property(get, set, construct_only)]
        pub(super) timestamp: OnceCell<Timestamp>,
        #[property(get, set, default)]
        pub(super) message_tag: Cell<MessageTag>,

        pub(super) inner: OnceCell<zbus::Message>,
        pub(super) receive_index: OnceCell<ReceiveIndex>,

        pub(super) associated_message: OnceCell<glib::WeakRef<super::Message>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Message {
        const NAME: &'static str = "BustleMessage";
        type Type = super::Message;
    }

    #[glib::derived_properties]
    impl ObjectImpl for Message {}
}

glib::wrapper! {
     pub struct Message(ObjectSubclass<imp::Message>);
}

impl Message {
    pub fn from_event(event: Event) -> Self {
        let this = glib::Object::builder::<Self>()
            .property(
                "message-type",
                MessageType::from(event.message.message_type()),
            )
            .property("timestamp", event.timestamp)
            .build();

        this.imp().inner.set(event.message).unwrap();

        this
    }

    pub fn to_event(&self) -> Event {
        Event {
            message: self.imp().inner.get().unwrap().clone(),
            timestamp: self.timestamp(),
        }
    }

    fn inner(&self) -> &zbus::Message {
        self.imp().inner.get().unwrap()
    }

    /// Return the length of the whole message
    pub fn len(&self) -> usize {
        self.inner().data().len()
    }

    /// Return the message body
    pub fn body(&self) -> zbus::message::Body {
        self.inner().body()
    }

    /// Return the message header
    pub fn header(&self) -> zbus::message::Header<'_> {
        self.inner().header()
    }

    /// Return the time it took to receive a response or None if it is a signal
    /// or no message is associated to the current one
    pub fn response_time(&self) -> Option<Timestamp> {
        let associated_message = self.associated_message()?;
        match self.message_type() {
            MessageType::Signal => None,
            MessageType::MethodReturn | MessageType::Error => {
                Some(self.timestamp() - associated_message.timestamp())
            }
            MessageType::MethodCall => Some(associated_message.timestamp() - self.timestamp()),
        }
    }

    pub fn set_receive_index(&self, raw_receive_index: u32) {
        self.imp()
            .receive_index
            .set(ReceiveIndex(raw_receive_index))
            .expect("receive index must only be set once");
    }

    pub fn receive_index(&self) -> ReceiveIndex {
        *self.imp().receive_index.get().unwrap()
    }

    pub fn set_associated_message(&self, message: &Self) {
        if self
            .imp()
            .associated_message
            .set(message.downgrade())
            .is_err()
        {
            // Method call can somehow be associated with multiple replies, so simply ignore
            // the subsequent ones.
            if self
                .associated_message()
                .is_some_and(|curr| curr.is_return_of(self) && message.is_return_of(self))
            {
                tracing::debug!("Associated message was already set to the same message");
            } else {
                unreachable!("associated message must only be set once");
            }
        }
    }

    pub fn associated_message(&self) -> Option<Self> {
        self.imp()
            .associated_message
            .get()
            .and_then(|m| m.upgrade())
    }

    pub fn sender_display(&self) -> String {
        if self.message_type().is_method_return() {
            self.associated_message()
                .as_ref()
                .and_then(|m| m.destination())
                .map(|d| d.to_string())
        } else {
            self.sender().map(|m| m.to_string())
        }
        .unwrap_or_else(|| "(no sender)".to_owned())
    }

    pub fn member_display(&self) -> String {
        if self.message_type().is_method_return() {
            self.associated_message()
                .as_ref()
                .map(|m| m.header())
                .and_then(|h| h.member().map(|m| m.to_string()))
        } else {
            self.header().member().map(|m| m.to_string())
        }
        .unwrap_or_else(|| "(no member)".to_owned())
    }

    pub fn interface_display(&self) -> String {
        if self.message_type().is_method_return() {
            self.associated_message()
                .as_ref()
                .map(|m| m.header())
                .and_then(|h| h.interface().map(|m| m.to_string()))
        } else {
            self.header().interface().map(|s| s.to_string())
        }
        .unwrap_or_else(|| "(no interface)".to_owned())
    }

    pub fn destination_display(&self) -> String {
        if self.message_type().is_method_return() {
            self.associated_message()
                .as_ref()
                .and_then(|m| m.sender())
                .map(|s| s.to_string())
        } else {
            self.destination().map(|m| m.to_string())
        }
        .unwrap_or_else(|| "(no destination)".to_owned())
    }

    pub fn path_display(&self) -> String {
        if self.message_type().is_method_return() {
            self.associated_message()
                .as_ref()
                .and_then(|m| m.header().path().map(|p| p.to_string()))
        } else {
            self.header().path().map(|p| p.to_string())
        }
        .unwrap_or_else(|| "(no path)".to_owned())
    }

    pub fn flags_display(&self) -> String {
        self.header()
            .primary()
            .flags()
            .iter()
            .map(|flag| match flag {
                zbus::message::Flags::NoReplyExpected => gettext("No Reply Expected"),
                zbus::message::Flags::NoAutoStart => gettext("No Auto Start"),
                zbus::message::Flags::AllowInteractiveAuth => {
                    gettext("Allow Interactive Authentication")
                }
            })
            .collect::<Vec<_>>()
            .join(" | ")
    }

    pub fn member_markup(&self, render_error: bool) -> String {
        let interface = self.interface_display();
        let member = self.member_display();

        if self.message_type() == MessageType::Error && render_error {
            // This is the .error class at
            // https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html#error-colors.
            let foreground = if adw::StyleManager::default().is_dark() {
                "#ff938c"
            } else {
                "#c30000"
            };
            format!(
                "<span foreground='{foreground}'>{}.<b>{}</b></span>",
                glib::markup_escape_text(&interface),
                glib::markup_escape_text(&member)
            )
        } else {
            format!(
                "{}.<b>{}</b>",
                glib::markup_escape_text(&interface),
                glib::markup_escape_text(&member)
            )
        }
    }

    pub fn sender(&self) -> Option<UniqueName<'_>> {
        self.header().sender().cloned()
    }

    pub fn destination(&self) -> Option<BusName<'_>> {
        self.header().destination().cloned().or_else(|| {
            match self.message_type() {
                MessageType::MethodCall => Some("method.call.destination"),
                MessageType::MethodReturn => Some("method.return.destination"),
                MessageType::Error => Some("method.error.destination"),
                _ => None,
            }
            .map(|raw_name| {
                let ret = BusName::WellKnown(WellKnownName::from_static_str(raw_name).unwrap());
                debug_assert!(Self::is_fallback_destination(&ret));
                ret
            })
        })
    }

    /// Whether `name` is used as a fallback destination internally
    pub fn is_fallback_destination(name: &BusName<'_>) -> bool {
        match name {
            BusName::Unique(_) => false,
            BusName::WellKnown(wk_name) => matches!(
                wk_name.as_str(),
                "method.call.destination"
                    | "method.return.destination"
                    | "method.error.destination"
            ),
        }
    }

    /// Whether this is the return/error message of `other`
    pub fn is_return_of(&self, other: &Self) -> bool {
        debug_assert!(self.message_type().is_method_return());

        other.message_type().is_method_call()
            && self.header().reply_serial() == Some(other.header().primary().serial_num())
            && self.destination() == other.sender().map(From::from)
    }

    pub fn print_as_busctl(
        &self,
        connection_type: Option<ConnectionType>,
    ) -> anyhow::Result<String> {
        use crate::busctl_cmd::{BusctlCommand, BusctlSubCommand};
        let sub_command = match self.message_type() {
            MessageType::Signal => BusctlSubCommand::Wait,
            MessageType::MethodCall => BusctlSubCommand::Call,
            MessageType::MethodReturn | MessageType::Error => unimplemented!(),
        };

        let body = self.body();
        // TODO Do we need this guard? Add a test for a method without an arg.
        let body_structure = if self.header().signature() != &zvariant::Signature::Unit {
            match body.deserialize::<zvariant::Structure<'_>>() {
                Ok(structure) => Some(structure),
                Err(err) => anyhow::bail!("Could not deserialize structure: {err}"),
            }
        } else {
            None
        };

        let header = self.header();
        let args = BusctlCommand {
            // NOTE: When reading a cap file there is no way to tell on which
            // bus we are. By default we assume it is a session connection
            connection_type: connection_type.unwrap_or(ConnectionType::Session),
            sub_command,
            destination: self.destination(),
            sender: self.sender(),
            path: header.path(),
            interface: header.interface(),
            member: header.member(),
            signature: header.signature(),
            body_structure,
        };

        args.into_command_string()
    }
}

impl std::fmt::Display for Message {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Message")
            .field("path", &self.path_display())
            .field("destination", &self.destination_display())
            .field("sender", &self.sender_display())
            .field("member", &self.member_display())
            .field("interface", &self.interface_display())
            .field("type", &self.message_type())
            .field("associated_message", &self.associated_message())
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use base64::prelude::*;

    use super::*;

    #[test]
    fn test_print_portal_call_as_busctl() {
        // This is a message captured with wireshark. The window handled comes
        // from test_shell_escape.
        let expected_cmd = "busctl --user call org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop org.freedesktop.portal.OpenURI OpenURI ssa{sv} 'wayland:1zb*ehkl'\\''MwqCXbcN-T=8EGr&7'\\!'zB'\\''_0' 'https://github.com/bilelmoussaoui/ashpd' 3 ask b false handle_token s ashpd_VoXzDnLLr8 writeable b false";
        let message_base64 = b"bAEAAbgAAAAoAAAApwAAAAEBbwAfAAAAL29yZy9mcmVlZGVza3RvcC9wb3J0YWwvZGVza3RvcAACAXMAHgAAAG9yZy5mcmVlZGVza3RvcC5wb3J0YWwuT3BlblVSSQAAAwFzAAcAAABPcGVuVVJJAAYBcwAeAAAAb3JnLmZyZWVkZXNrdG9wLnBvcnRhbC5EZXNrdG9wAAAIAWcAB3NzYXtzdn0AAAAABwFzAAYAAAA6MS42NTcAACgAAAB3YXlsYW5kOjF6YiplaGtsJ013cUNYYmNOLVQ9OEVHciY3IXpCJ18wAAAAACcAAABodHRwczovL2dpdGh1Yi5jb20vYmlsZWxtb3Vzc2FvdWkvYXNocGQAWAAAAAwAAABoYW5kbGVfdG9rZW4AAXMAEAAAAGFzaHBkX1ZvWHpEbkxMcjgAAAAAAAAAAAkAAAB3cml0ZWFibGUAAWIAAAAAAAAAAAMAAABhc2sAAWIAAAAAAAA=";

        let msg = message(message_base64).unwrap();
        let cmd = msg.print_as_busctl(Some(ConnectionType::Session)).unwrap();

        assert_eq!(cmd, expected_cmd);
    }

    #[test]
    fn test_print_ping_as_busctl() {
        // Tests a call without an argument
        let expected_cmd = "busctl --user call org.freedesktop.secrets /org/freedesktop/DBus org.freedesktop.DBus.Peer Ping";
        let message_base64 = b"bAEEAQAAAAACAAAAiAAAAAEBbwAVAAAAL29yZy9mcmVlZGVza3RvcC9EQnVzAAAAAwFzAAQAAABQaW5nAAAAAAIBcwAZAAAAb3JnLmZyZWVkZXNrdG9wLkRCdXMuUGVlcgAAAAAAAAAGAXMAFwAAAG9yZy5mcmVlZGVza3RvcC5zZWNyZXRzAAcBcwAHAAAAOjEuMjYyMgA=";

        let msg = message(message_base64).unwrap();
        let cmd = msg.print_as_busctl(Some(ConnectionType::Session)).unwrap();

        assert_eq!(cmd, expected_cmd);
    }

    fn message(message_base64: &[u8]) -> anyhow::Result<Message> {
        let message_bytes = BASE64_STANDARD.decode(message_base64)?;
        let event = Event::from_bytes(message_bytes, Timestamp::now())?;
        Ok(Message::from_event(event))
    }
}
