//! xdg-desktop-portal backend D-Bus interfaces.
//!
//! Implements the portal interfaces needed for Flatpak applications:
//! - org.freedesktop.impl.portal.Screenshot
//! - org.freedesktop.impl.portal.ScreenCast

use std::collections::HashMap;

use calloop::channel::Sender;
use calloop::LoopHandle;
use tracing::info;
use zbus::zvariant::{Array, ObjectPath, OwnedObjectPath, OwnedValue, Signature, Str, Value};

use crate::forge::Forge;

/// Result of a portal request routed through the consent dialog.
#[derive(Debug)]
pub enum PortalReply {
    Uri(String),
    Color(f64, f64, f64),
    Denied,
    Failed,
}

/// Events from the portal D-Bus, each carrying a reply channel the main loop
/// completes once the user has answered the consent dialog.
#[derive(Debug)]
pub enum PortalEvent {
    Screenshot {
        app_id: String,
        reply: async_channel::Sender<PortalReply>,
    },
    Screencast {
        app_id: String,
        /// The portal session that owns the cast; its Close stops only this one.
        session: String,
        /// Replies with the PipeWire node id to stream from, or `None` when
        /// denied or unavailable.
        reply: async_channel::Sender<Option<u32>>,
    },
    PickColor {
        app_id: String,
        reply: async_channel::Sender<PortalReply>,
    },
    /// A screencast session was closed; tear down its cast if it owns the
    /// active one.
    ScreencastStop { session: String },
}

/// Start the xdg-desktop-portal backend D-Bus server.
pub fn start_server(event_loop: &LoopHandle<'static, Forge>) {
    crate::services::dbus_worker::spawn_adapter(
        event_loop,
        "forge-portal-dbus",
        run_portal_dbus,
        |ev, forge| match ev {
            PortalEvent::Screenshot { app_id, reply } => {
                forge.request_screenshot_consent(app_id, reply);
            }
            PortalEvent::Screencast { app_id, session, reply } => {
                forge.request_screencast_consent(app_id, session, reply);
            }
            PortalEvent::PickColor { app_id, reply } => {
                forge.request_pickcolor_consent(app_id, reply);
            }
            PortalEvent::ScreencastStop { session } => {
                if let Some(pw) = forge.pipewire.as_mut() {
                    pw.stop_cast_for(&session);
                }
            }
        },
    );
}

fn run_portal_dbus(
    sender: Sender<PortalEvent>,
) -> Result<(), Box<dyn std::error::Error>> {
    info!("Portal D-Bus backend starting");

    let screenshot_iface = ScreenshotPortalIface {
        sender: sender.clone(),
    };
    let screencast_iface = ScreenCastPortalIface {
        sender: sender.clone(),
    };

    let _conn = zbus::blocking::ConnectionBuilder::session()?
        .name("org.freedesktop.impl.portal.desktop.forge")?
        .serve_at(
            "/org/freedesktop/portal/desktop/forge",
            screenshot_iface,
        )?
        .serve_at(
            "/org/freedesktop/portal/desktop/forge",
            screencast_iface,
        )?
        .build()?;

    loop {
        std::thread::sleep(std::time::Duration::from_secs(60));
    }
}

struct ScreenshotPortalIface {
    sender: Sender<PortalEvent>,
}

#[zbus::interface(name = "org.freedesktop.impl.portal.Screenshot")]
impl ScreenshotPortalIface {
    async fn screenshot(
        &self,
        _handle: OwnedObjectPath,
        app_id: String,
        _parent_window: String,
        _options: HashMap<String, OwnedValue>,
    ) -> (u32, HashMap<String, OwnedValue>) {
        let (tx, rx) = async_channel::bounded(1);
        if self.sender.send(PortalEvent::Screenshot {
            app_id,
            reply: tx,
        }).is_err() {
            return (2, HashMap::new());
        }

        match rx.recv().await {
            Ok(PortalReply::Uri(uri)) => {
                let mut results = HashMap::new();
                results.insert("uri".to_string(), OwnedValue::from(Str::from(uri)));
                (0, results)
            }
            Ok(PortalReply::Denied) => (1, HashMap::new()),
            _ => (2, HashMap::new()),
        }
    }

    async fn pick_color(
        &self,
        _handle: ObjectPath<'_>,
        app_id: &str,
        _parent_window: &str,
        _options: HashMap<String, OwnedValue>,
    ) -> (u32, HashMap<String, OwnedValue>) {
        let (tx, rx) = async_channel::bounded(1);
        if self.sender.send(PortalEvent::PickColor {
            app_id: app_id.to_string(),
            reply: tx,
        }).is_err() {
            return (2, HashMap::new());
        }

        match rx.recv().await {
            Ok(PortalReply::Color(r, g, b)) => {
                let color = Value::Structure((r, g, b).into());
                match OwnedValue::try_from(color) {
                    Ok(owned) => {
                        let mut results = HashMap::new();
                        results.insert("color".to_string(), owned);
                        (0, results)
                    }
                    Err(_) => (2, HashMap::new()),
                }
            }
            Ok(PortalReply::Denied) => (1, HashMap::new()),
            _ => (2, HashMap::new()),
        }
    }
}

struct ScreenCastPortalIface {
    sender: Sender<PortalEvent>,
}

#[zbus::interface(name = "org.freedesktop.impl.portal.ScreenCast")]
impl ScreenCastPortalIface {
    async fn create_session(
        &self,
        #[zbus(object_server)] server: &zbus::ObjectServer,
        _handle: ObjectPath<'_>,
        session_handle: ObjectPath<'_>,
        _app_id: &str,
        _options: HashMap<String, OwnedValue>,
    ) -> (u32, HashMap<String, OwnedValue>) {
        static SESSION_IDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
        let n = SESSION_IDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        info!("ScreenCast: create_session {session_handle}");

        // Serve the Session object so the frontend can Close it (the cast is
        // torn down there); without it the frontend aborts the whole request.
        let session = SessionIface {
            sender: self.sender.clone(),
            path: session_handle.to_string(),
        };
        if server.at(&session_handle, session).await.is_err() {
            return (2, HashMap::new());
        }

        let mut results = HashMap::new();
        results.insert(
            "session_id".to_string(),
            OwnedValue::from(Str::from(format!("forge-screencast-{n}"))),
        );
        (0, results)
    }

    fn select_sources(
        &self,
        _handle: ObjectPath<'_>,
        _session_handle: ObjectPath<'_>,
        _app_id: &str,
        _options: HashMap<String, OwnedValue>,
    ) -> (u32, HashMap<String, OwnedValue>) {
        info!("ScreenCast: select_sources");
        (0, HashMap::new())
    }

    async fn start(
        &self,
        #[zbus(object_server)] server: &zbus::ObjectServer,
        _handle: OwnedObjectPath,
        session_handle: OwnedObjectPath,
        app_id: String,
        _parent_window: String,
        _options: HashMap<String, OwnedValue>,
    ) -> (u32, HashMap<String, OwnedValue>) {
        // The session must exist and not be closed (Close removes its object).
        if server.interface::<_, SessionIface>(&*session_handle).await.is_err() {
            return (2, HashMap::new());
        }

        let (tx, rx) = async_channel::bounded(1);
        if self.sender.send(PortalEvent::Screencast {
            app_id,
            session: session_handle.to_string(),
            reply: tx,
        }).is_err() {
            return (2, HashMap::new());
        }

        let node_id = match rx.recv().await {
            Ok(Some(id)) => id,
            _ => return (2, HashMap::new()),
        };

        // One stream: (node_id, { }). The client opens the PipeWire remote via
        // the frontend portal and connects to this node.
        let props: HashMap<String, Value> = HashMap::new();
        let stream = Value::Structure((node_id, props).into());
        let mut streams = Array::new(Signature::from_static_str_unchecked("(ua{sv})"));
        let _ = streams.append(stream);
        let streams = Value::Array(streams);
        match OwnedValue::try_from(streams) {
            Ok(owned) => {
                let mut results = HashMap::new();
                results.insert("streams".to_string(), owned);
                (0, results)
            }
            Err(_) => (2, HashMap::new()),
        }
    }

    #[zbus(property)]
    fn available_source_types(&self) -> u32 {
        // 1 = monitor, 2 = window, 4 = virtual
        1 // Only monitor capture supported
    }

    #[zbus(property)]
    fn available_cursor_modes(&self) -> u32 {
        // 1 = hidden, 2 = embedded, 4 = metadata
        1 | 2
    }

    #[zbus(property)]
    fn version(&self) -> u32 {
        4
    }
}

struct SessionIface {
    sender: Sender<PortalEvent>,
    path: String,
}

#[zbus::interface(name = "org.freedesktop.impl.portal.Session")]
impl SessionIface {
    async fn close(
        &self,
        #[zbus(object_server)] server: &zbus::ObjectServer,
        #[zbus(header)] header: zbus::message::Header<'_>,
    ) {
        info!("ScreenCast: session closed: {}", self.path);
        let _ = self.sender.send(PortalEvent::ScreencastStop { session: self.path.clone() });
        if let Some(path) = header.path() {
            let _ = server.remove::<Self, _>(path).await;
        }
    }

    #[zbus(property)]
    fn version(&self) -> u32 {
        2
    }
}
