//! P5-36 — Windows taskbar Jump List ("Recent Rooms").
//!
//! Publishes a custom Jump List category so users can right-click the taskbar
//! (or Start) icon and jump straight into a recently-active room. The web client
//! calls `set_jump_list([{ title, uri }])` from `useTauriJumpList` whenever its
//! recent-room list changes; each `uri` is a `matrix:` deep link.
//!
//! Windows: builds an `ICustomDestinationList` with an `IObjectCollection` of
//! `IShellLinkW` task links. Each link relaunches the current executable with the
//! room's `matrix:` URI as its single argument — the existing deep-link handler
//! in lib.rs (`forward_deeplink` → `lotus-deeplink`) then routes it to the room.
//! The link's visible label is set via `IPropertyStore` + `PKEY_Title`
//! (System.Title) using a `PROPVARIANT`.
//!
//! COM here runs on the command's (thread-pool) thread, so we initialize an STA
//! apartment with `CoInitializeEx` and balance it with `CoUninitialize` only when
//! we were the ones that initialized it (mirrors the COM usage in
//! `set_badge_count`). All COM interfaces are scoped so they release before the
//! apartment is torn down.
//!
//! Other platforms are a no-op (macOS has no direct equivalent; Linux desktop
//! files differ) — the command stays cross-platform so the web side is
//! unconditional.

use tauri::AppHandle;

/// One Jump List entry supplied by the web client. `uri` is a `matrix:` deep
/// link accepted by the deep-link handler in lib.rs.
#[derive(serde::Deserialize)]
pub struct JumpItem {
    pub title: String,
    pub uri: String,
}

#[tauri::command]
pub fn set_jump_list(app: AppHandle, items: Vec<JumpItem>) -> Result<(), String> {
    #[cfg(target_os = "windows")]
    {
        // `app` is unused on Windows (COM runs on the calling thread); bind it so
        // the signature stays identical cross-platform and no warning fires.
        let _ = &app;

        use std::os::windows::ffi::OsStrExt;
        use windows::{
            core::{w, Interface, PCWSTR},
            Win32::{
                Storage::EnhancedStorage::PKEY_Title,
                System::Com::{
                    CoCreateInstance, CoInitializeEx, CoUninitialize,
                    StructuredStorage::PROPVARIANT, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
                },
                UI::Shell::{
                    // IObjectArray/IObjectCollection live in Shell::Common
                    // (feature Win32_UI_Shell_Common), NOT Shell or System::Com.
                    Common::{IObjectArray, IObjectCollection},
                    DestinationList, EnumerableObjectCollection, ICustomDestinationList,
                    IShellLinkW, PropertiesSystem::IPropertyStore, ShellLink,
                },
            },
        };

        // Wide, NUL-terminated path to the running executable; reused for every
        // link's target and icon. Computed before touching COM so a failure here
        // doesn't leak an initialized apartment.
        let exe = std::env::current_exe().map_err(|e| e.to_string())?;
        let exe_wide: Vec<u16> = exe
            .as_os_str()
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();

        // STA is required for the shell Jump List objects. S_OK means we
        // initialized (and must uninitialize); S_FALSE means it was already
        // initialized on this thread (still balance it); RPC_E_CHANGED_MODE (an
        // error) means don't touch it. Note: `unsafe` does not reach into the
        // closure below, so its body carries its own `unsafe` block; the COM
        // interfaces it creates are all released (dropped) before we uninitialize.
        let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };

        let result = (|| -> windows::core::Result<()> {
            unsafe {
                let list: ICustomDestinationList =
                    CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER)?;

                if items.is_empty() {
                    // Nothing to show — clear any list we previously published.
                    list.DeleteList(PCWSTR::null())?;
                    return Ok(());
                }

                // BeginList hands back the items the user manually removed; we
                // don't re-add anything, so we can ignore it. `min_slots` is the
                // max entries the shell will display.
                let mut min_slots: u32 = 0;
                let _removed: IObjectArray = list.BeginList(&mut min_slots)?;

                let collection: IObjectCollection =
                    CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;

                for item in &items {
                    let link: IShellLinkW =
                        CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;

                    // Relaunch this exe with the matrix: URI as its only argument.
                    link.SetPath(PCWSTR(exe_wide.as_ptr()))?;
                    let arg_wide: Vec<u16> =
                        item.uri.encode_utf16().chain(std::iter::once(0)).collect();
                    link.SetArguments(PCWSTR(arg_wide.as_ptr()))?;
                    // Use the app's own icon for the entry.
                    link.SetIconLocation(PCWSTR(exe_wide.as_ptr()), 0)?;

                    // The visible label comes from System.Title on the link's
                    // property store (a bare IShellLink has no display name).
                    let store: IPropertyStore = link.cast()?;
                    // From<&str> builds a VT_LPWSTR PROPVARIANT (what System.Title expects).
                    let title = PROPVARIANT::from(item.title.as_str());
                    store.SetValue(&PKEY_Title, &title)?;
                    store.Commit()?;

                    collection.AddObject(&link)?;
                }

                let array: IObjectArray = collection.cast()?;
                list.AppendCategory(w!("Recent Rooms"), &array)?;
                list.CommitList()?;
                Ok(())
            }
        })();

        // All interfaces above are dropped (released) by the time we get here, so
        // it's safe to tear the apartment down.
        if hr.is_ok() {
            unsafe { CoUninitialize() };
        }

        result.map_err(|e| e.to_string())?;
    }

    #[cfg(not(target_os = "windows"))]
    {
        // No-op on non-Windows platforms (see module docs). Bind the args so the
        // signature stays identical cross-platform and no unused warnings fire.
        let _ = (&app, &items);
    }

    Ok(())
}
