use anyhow::Context;
use zbus::{
    names::{BusName, InterfaceName, MemberName, UniqueName},
    zvariant::{self, ObjectPath, Signature},
};

use crate::ConnectionType;

pub enum BusctlSubCommand {
    Call,
    Wait,
}

impl BusctlSubCommand {
    fn as_str(&self) -> &str {
        match self {
            Self::Call => "call",
            Self::Wait => "wait",
        }
    }
}

pub struct BusctlCommand<'a> {
    pub sub_command: BusctlSubCommand,
    pub connection_type: ConnectionType,
    pub sender: Option<UniqueName<'a>>,
    pub destination: Option<BusName<'a>>,
    pub path: Option<&'a ObjectPath<'a>>,
    pub interface: Option<&'a InterfaceName<'a>>,
    pub body_structure: Option<zvariant::Structure<'a>>,
    pub member: Option<&'a MemberName<'a>>,
    pub signature: &'a Signature,
}

impl<'a> BusctlCommand<'a> {
    pub fn into_command_string(self) -> anyhow::Result<String> {
        let mut cmd = vec!["busctl"];

        let session_flag = match &self.connection_type {
            ConnectionType::Session => "--user",
            ConnectionType::System => "",
            ConnectionType::Address(addr) => &format!("--address={addr}"),
        };

        if !session_flag.is_empty() {
            cmd.push(session_flag);
        }
        cmd.push(self.sub_command.as_str());

        match self.sub_command {
            BusctlSubCommand::Call => {
                let destination = self
                    .destination
                    .as_ref()
                    .context("Message does not have a destination")?;
                cmd.push(destination);
            }
            BusctlSubCommand::Wait => {
                let sender = self
                    .sender
                    .as_ref()
                    .context("Message does not have a sender")?;
                cmd.push(sender);
            }
        }
        let path = self.path.context("Message does not have a path")?;
        cmd.push(path);

        let interface = self
            .interface
            .context("Message does not have a interface")?;
        cmd.push(interface);

        let member = self.member.context("Message does not have a member")?;
        cmd.push(member);

        if matches!(self.sub_command, BusctlSubCommand::Call)
            && *self.signature != zvariant::Signature::Unit
        {
            if let Some(structure) = self.body_structure {
                let mut body = String::new();
                crate::message_fmt::structure_display_fmt(&mut body, &structure)?;
                let signature = self.signature.to_string_no_parens();
                if !body.is_empty() {
                    cmd.push(&signature);
                    cmd.push(&body);
                }
                Ok(cmd.join(" "))
            } else {
                anyhow::bail!("Message does not have a body");
            }
        } else {
            Ok(cmd.join(" "))
        }
    }
}

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

    #[test]
    fn test_man_busctl_example() {
        // From busctl(1) examples.
        let expected = "busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager StartUnit ss cups.service replace";

        let body_structure = zvariant::Structure::from((
            zvariant::Str::from_static("cups.service"),
            zvariant::Str::from_static("replace"),
        ));
        let path = ObjectPath::from_static_str("/org/freedesktop/systemd1").unwrap();
        let iface = InterfaceName::from_static_str("org.freedesktop.systemd1.Manager").unwrap();
        let member = MemberName::from_static_str("StartUnit").unwrap();
        let cmd = BusctlCommand {
            sub_command: BusctlSubCommand::Call,
            connection_type: ConnectionType::System,
            sender: None,
            destination: Some(BusName::from_static_str("org.freedesktop.systemd1").unwrap()),
            path: Some(&path),
            interface: Some(&iface),
            member: Some(&member),
            signature: &body_structure.signature().clone(),
            body_structure: Some(body_structure),
        };

        assert_eq!(cmd.into_command_string().unwrap(), expected);
    }
}
