use super::NotificationIcon;
use log::{debug, info, warn};
use std::collections::HashMap;
use std::time::SystemTime;
use tokio::sync::broadcast;
use zbus::{
    Connection,
    fdo::{DBusProxy, RequestNameFlags, RequestNameReply},
    interface,
    names::WellKnownName,
    zvariant::OwnedValue,
};

#[derive(Debug, Clone)]
pub enum NotificationEvent {
    Received(Box<Notification>),
    Closed(u32),
}

/// Which instance of an id a close applies to.
#[derive(Debug, Clone, Copy)]
pub enum CloseGuard {
    /// Whatever holds the id now.
    Any,
    /// Only while the id still carries this revision.
    Revision(u64),
}

const NAME: WellKnownName =
    WellKnownName::from_static_str_unchecked("org.freedesktop.Notifications");
pub const OBJECT_PATH: &str = "/org/freedesktop/Notifications";

/// Notification urgency as defined by the freedesktop Notifications
/// specification (sent as the `urgency` byte hint).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum Urgency {
    Low,
    #[default]
    Normal,
    Critical,
}

impl Urgency {
    fn from_hints(hints: &HashMap<String, OwnedValue>) -> Self {
        hints
            .get("urgency")
            .and_then(|v| u8::try_from(v).ok())
            .map(|b| match b {
                0 => Self::Low,
                2 => Self::Critical,
                _ => Self::Normal,
            })
            .unwrap_or_default()
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Notification {
    pub id: u32,
    /// Bumped on every `Notify`, so a consumer can tell one instance of an id
    /// from the next one a `replaces_id` put in its place.
    pub revision: u64,
    pub app_name: String,
    pub app_icon: String,
    pub summary: String,
    pub body: String,
    pub actions: Vec<String>,
    pub hints: HashMap<String, OwnedValue>,
    pub expire_timeout: i32,
    pub timestamp: SystemTime,
    pub urgency: Urgency,
    #[serde(skip)]
    pub icon: Option<NotificationIcon>,
}

pub struct NotificationDaemon {
    next_id: u32,
    next_revision: u64,
    revisions: HashMap<u32, u64>,
    event_tx: broadcast::Sender<NotificationEvent>,
    connection: Connection,
}

impl NotificationDaemon {
    pub fn new(event_tx: broadcast::Sender<NotificationEvent>, connection: Connection) -> Self {
        Self {
            next_id: 0,
            next_revision: 0,
            revisions: HashMap::new(),
            event_tx,
            connection,
        }
    }
}

#[interface(name = "org.freedesktop.Notifications")]
impl NotificationDaemon {
    async fn get_capabilities(&self) -> Vec<String> {
        vec![
            "body".to_string(),
            "body-markup".to_string(),
            "icon-static".to_string(),
            "actions".to_string(),
        ]
    }

    #[allow(clippy::too_many_arguments)]
    async fn notify(
        &mut self,
        app_name: String,
        replaces_id: u32,
        app_icon: String,
        summary: String,
        body: String,
        actions: Vec<String>,
        hints: HashMap<String, OwnedValue>,
        expire_timeout: i32,
    ) -> u32 {
        let id = if replaces_id == 0 {
            self.next_id = self.next_id.checked_add(1).unwrap_or(1);
            self.next_id
        } else {
            self.next_id = self.next_id.max(replaces_id);
            replaces_id
        };

        self.next_revision += 1;
        let revision = self.next_revision;
        self.revisions.insert(id, revision);

        let icon = NotificationIcon::resolve(&app_name, &app_icon, &hints);
        let urgency = Urgency::from_hints(&hints);
        let notification = Notification {
            id,
            revision,
            app_name,
            app_icon,
            summary,
            body,
            actions,
            hints,
            expire_timeout,
            timestamp: SystemTime::now(),
            urgency,
            icon,
        };

        debug!("New notification: {:?}", notification);

        // Send event through channel
        let _ = self
            .event_tx
            .send(NotificationEvent::Received(Box::new(notification)));

        id
    }

    async fn close_notification(&mut self, id: u32) {
        self.revisions.remove(&id);

        // Send event through channel
        let _ = self.event_tx.send(NotificationEvent::Closed(id));

        // Emit DBus signal for external applications
        let _ = self
            .connection
            .emit_signal(
                None::<&str>,
                OBJECT_PATH,
                NAME.as_str(),
                "NotificationClosed",
                &(id, 3u32),
            )
            .await;
    }

    async fn get_server_information(&self) -> (String, String, String, String) {
        (
            "ashell".to_string(),
            "MalpenZibo".to_string(),
            "0.1".to_string(),
            "1.2".to_string(),
        )
    }
}

impl NotificationDaemon {
    pub async fn start_server() -> anyhow::Result<(Connection, broadcast::Sender<NotificationEvent>)>
    {
        Self::init_server().await
    }

    async fn init_server() -> anyhow::Result<(Connection, broadcast::Sender<NotificationEvent>)> {
        let (event_tx, _rx) = broadcast::channel(100);

        let connection = zbus::connection::Connection::session().await?;
        let daemon = NotificationDaemon::new(event_tx.clone(), connection.clone());
        connection.object_server().at(OBJECT_PATH, daemon).await?;

        let dbus_proxy = DBusProxy::new(&connection).await?;
        let flags = RequestNameFlags::AllowReplacement | RequestNameFlags::ReplaceExisting;
        match dbus_proxy.request_name(NAME, flags).await? {
            RequestNameReply::PrimaryOwner | RequestNameReply::AlreadyOwner => {
                info!("Acquired notification daemon bus name");
            }
            RequestNameReply::InQueue | RequestNameReply::Exists => {
                warn!("Bus name '{NAME}' already owned");
            }
        }

        Ok((connection, event_tx))
    }

    /// `guard` pins the instance the caller acted on: both steps are skipped
    /// when `id` no longer carries it, because a `Notify` won the race against
    /// this call and the notification the user acted on is gone. Closing anyway
    /// would take the replacement down with it. Holding the interface lock over
    /// the check and both steps is what keeps that ordering decidable.
    pub async fn invoke_and_close(
        connection: &Connection,
        id: u32,
        action_key: Option<String>,
        guard: CloseGuard,
    ) -> anyhow::Result<()> {
        let iface_ref = connection
            .object_server()
            .interface::<_, NotificationDaemon>(OBJECT_PATH)
            .await?;
        let mut daemon = iface_ref.get_mut().await;

        if let CloseGuard::Revision(revision) = guard
            && daemon.revisions.get(&id) != Some(&revision)
        {
            debug!("Skipping close of notification {}: replaced since", id);
            return Ok(());
        }

        if let Some(action_key) = action_key {
            connection
                .emit_signal(
                    None::<&str>,
                    OBJECT_PATH,
                    NAME.as_str(),
                    "ActionInvoked",
                    &(id, action_key),
                )
                .await?;
        }

        daemon.close_notification(id).await;
        Ok(())
    }
}
