#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

use tauri::{
    menu::{Menu, MenuItem, PredefinedMenuItem},
    tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
    webview::{NewWindowResponse, PageLoadEvent, WebviewWindowBuilder},
    Manager, WebviewUrl,
};
use tauri_plugin_opener::OpenerExt;

/// Bring the main window to the foreground from the tray / a hidden /
/// minimized state. Shared by the tray, single-instance, and deep-link paths.
fn show_main(app: &tauri::AppHandle) {
    if let Some(window) = app.get_webview_window("main") {
        let _ = window.show();
        let _ = window.unminimize();
        let _ = window.set_focus();
    }
}

/// Hand a `matrix:` / `matrix.to` URL to the web app by dispatching a DOM
/// CustomEvent the client listens for (see useDeepLinkNavigate.ts). Uses
/// `eval` so we don't need the @tauri-apps/api event package on the web side.
fn forward_deeplink(app: &tauri::AppHandle, url: &str) {
    show_main(app);
    if let Some(window) = app.get_webview_window("main") {
        if let Ok(json) = serde_json::to_string(url) {
            let _ = window.eval(&format!(
                "window.dispatchEvent(new CustomEvent('lotus-deeplink',{{detail:{json}}}))"
            ));
        }
    }
}

/// Pull the first `matrix:` link out of a process's CLI args (Windows/Linux
/// pass deep-link URLs as argv to a freshly launched instance).
fn matrix_url_from_args(args: &[String]) -> Option<String> {
    args.iter().find(|a| a.starts_with("matrix:")).cloned()
}

// Injected into every page before app scripts load.
// Patches window.Notification to route through tauri-plugin-notification so
// WebView2's default "denied" state never reaches cinny's permission check.
// Also patches navigator.permissions.query so the React hook sees "granted".
const NOTIFICATION_BRIDGE: &str = r#"(function(){
  function TauriNotification(title,options){
    var opts=options||{};
    try{
      window.__TAURI_INTERNALS__.invoke('send_notification',{
        title:String(title),
        body:opts.body!=null?String(opts.body):undefined
      }).catch(function(){});
    }catch(_){}
  }
  TauriNotification.prototype=Object.create(EventTarget.prototype);
  TauriNotification.prototype.constructor=TauriNotification;
  TauriNotification.prototype.close=function(){};
  Object.defineProperty(TauriNotification,'permission',{get:function(){return 'granted';},configurable:true});
  TauriNotification.requestPermission=function(){return Promise.resolve('granted');};
  TauriNotification.maxActions=0;
  Object.defineProperty(window,'Notification',{value:TauriNotification,writable:true,configurable:true});
  var _q=navigator.permissions.query.bind(navigator.permissions);
  navigator.permissions.query=function(desc){
    if(desc&&desc.name==='notifications'){
      return Promise.resolve(Object.assign(new EventTarget(),{state:'granted',onchange:null}));
    }
    return _q(desc);
  };
})();"#;

#[tauri::command]
fn send_notification(
    app: tauri::AppHandle,
    title: String,
    body: Option<String>,
) -> Result<(), String> {
    use tauri_plugin_notification::NotificationExt;
    let mut builder = app.notification().builder().title(&title);
    if let Some(b) = &body {
        builder = builder.body(b);
    }
    builder.show().map_err(|e| e.to_string())
}

#[derive(serde::Serialize)]
struct UpdateInfo {
    available: bool,
    version: Option<String>,
}

#[tauri::command]
async fn check_for_update(app: tauri::AppHandle) -> Result<UpdateInfo, String> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        use tauri_plugin_updater::UpdaterExt;
        return match app.updater().map_err(|e| e.to_string())?.check().await {
            Ok(Some(update)) => Ok(UpdateInfo { available: true, version: Some(update.version) }),
            Ok(None) => Ok(UpdateInfo { available: false, version: None }),
            Err(e) => Err(e.to_string()),
        };
    }
    #[cfg(any(target_os = "android", target_os = "ios"))]
    Ok(UpdateInfo { available: false, version: None })
}

#[tauri::command]
async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        use tauri_plugin_updater::UpdaterExt;
        if let Some(update) = app
            .updater()
            .map_err(|e| e.to_string())?
            .check()
            .await
            .map_err(|e| e.to_string())?
        {
            update
                .download_and_install(|_chunk, _total| {}, || {})
                .await
                .map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}

#[tauri::command]
fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
    #[cfg(target_os = "windows")]
    {
        use windows::{
            core::{BOOL, PCWSTR},
            Win32::{
                Foundation::{COLORREF, HWND, RECT},
                Graphics::Gdi::{
                    CreateBitmap, CreateCompatibleDC, CreateDIBSection, CreateFontW, CreatePen,
                    CreateSolidBrush, DeleteDC, DeleteObject, DIB_RGB_COLORS, DrawTextW, Ellipse,
                    ReleaseDC, SelectObject, SetBkMode, SetTextColor, BITMAPINFO,
                    BITMAPINFOHEADER, BI_RGB, CLIP_DEFAULT_PRECIS, DEFAULT_CHARSET, DEFAULT_PITCH,
                    DEFAULT_QUALITY, DT_CENTER, DT_SINGLELINE, DT_VCENTER, FF_DONTCARE,
                    FW_BOLD, OUT_DEFAULT_PRECIS, PS_NULL, TRANSPARENT,
                },
                UI::{
                    Shell::{ITaskbarList3, TaskbarList},
                    WindowsAndMessaging::{CreateIconIndirect, DestroyIcon, HICON, ICONINFO},
                },
                System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
            },
        };

        let hwnd = HWND(window.hwnd().map_err(|e| e.to_string())?.0 as _);

        let hicon: Option<HICON> = if count > 0 {
            let label = if count > 99 {
                "99+".to_string()
            } else {
                count.to_string()
            };
            let mut label_wide: Vec<u16> = label.encode_utf16().chain(std::iter::once(0)).collect();

            unsafe {
                let size = 20i32;
                let hdc_screen = windows::Win32::Graphics::Gdi::GetDC(None);
                let hdc = CreateCompatibleDC(Some(hdc_screen));

                let bmi = BITMAPINFO {
                    bmiHeader: BITMAPINFOHEADER {
                        biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
                        biWidth: size,
                        biHeight: -size,
                        biPlanes: 1,
                        biBitCount: 32,
                        biCompression: BI_RGB.0,
                        ..Default::default()
                    },
                    ..Default::default()
                };
                let mut bits: *mut std::ffi::c_void = std::ptr::null_mut();
                let hbm_color =
                    CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0)
                        .map_err(|e| e.to_string())?;
                // Zero-init so undrawn pixels are fully transparent (CreateDIBSection
                // does not guarantee zeroed memory; garbage bytes cause a black square).
                if !bits.is_null() {
                    std::ptr::write_bytes(bits as *mut u8, 0, (size * size * 4) as usize);
                }
                let old_bm = SelectObject(hdc, hbm_color.into());

                let hbrush = CreateSolidBrush(COLORREF(0x003030DD));
                let old_brush = SelectObject(hdc, hbrush.into());
                let hpen = CreatePen(PS_NULL, 0, COLORREF(0));
                let old_pen = SelectObject(hdc, hpen.into());
                let _ = Ellipse(hdc, 0, 0, size, size);

                let hfont = CreateFontW(
                    14,
                    0,
                    0,
                    0,
                    FW_BOLD.0 as i32,
                    0,
                    0,
                    0,
                    DEFAULT_CHARSET,
                    OUT_DEFAULT_PRECIS,
                    CLIP_DEFAULT_PRECIS,
                    DEFAULT_QUALITY,
                    (DEFAULT_PITCH.0 | FF_DONTCARE.0) as u32,
                    windows::core::w!("Segoe UI"),
                );
                let old_font = SelectObject(hdc, hfont.into());
                SetTextColor(hdc, COLORREF(0x00FFFFFF));
                let _ = SetBkMode(hdc, TRANSPARENT);
                let mut rect = RECT { left: 0, top: 0, right: size, bottom: size };
                let label_len = label_wide.len() - 1;
                let _ = DrawTextW(
                    hdc,
                    &mut label_wide[..label_len],
                    &mut rect,
                    DT_CENTER | DT_VCENTER | DT_SINGLELINE,
                );

                SelectObject(hdc, old_brush);
                SelectObject(hdc, old_pen);
                SelectObject(hdc, old_font);
                SelectObject(hdc, old_bm);
                let _ = DeleteObject(hbrush.into());
                let _ = DeleteObject(hpen.into());
                let _ = DeleteObject(hfont.into());

                // GDI drawing leaves the alpha channel at 0 for all pixels.
                // Set alpha=255 for every painted pixel so Windows uses per-pixel
                // alpha compositing instead of falling back to the opaque mask,
                // which would render unpainted corner pixels as a black square.
                let pixel_count = (size * size) as usize;
                let pixels =
                    std::slice::from_raw_parts_mut(bits as *mut u32, pixel_count);
                for pixel in pixels.iter_mut() {
                    if *pixel != 0 {
                        *pixel |= 0xFF00_0000u32;
                    }
                }

                let hbm_mask = CreateBitmap(size, size, 1, 1, None);
                if hbm_mask.0 as usize == 0 {
                    let _ = DeleteObject(hbm_color.into());
                    return Err("CreateBitmap failed".to_string());
                }

                let icon_info = ICONINFO {
                    fIcon: BOOL(1),
                    xHotspot: 0,
                    yHotspot: 0,
                    hbmMask: hbm_mask,
                    hbmColor: hbm_color,
                };
                let hicon = CreateIconIndirect(&icon_info).map_err(|e| {
                    let _ = DeleteObject(hbm_color.into());
                    let _ = DeleteObject(hbm_mask.into());
                    e.to_string()
                })?;

                let _ = DeleteObject(hbm_color.into());
                let _ = DeleteObject(hbm_mask.into());
                let _ = DeleteDC(hdc);
                let _ = ReleaseDC(None, hdc_screen);

                Some(hicon)
            }
        } else {
            None
        };

        unsafe {
            let taskbar: ITaskbarList3 =
                CoCreateInstance(&TaskbarList, None, CLSCTX_INPROC_SERVER)
                    .map_err(|e| e.to_string())?;
            taskbar.HrInit().map_err(|e| e.to_string())?;
            taskbar
                .SetOverlayIcon(hwnd, hicon.unwrap_or_default(), PCWSTR::null())
                .map_err(|e| e.to_string())?;
            if let Some(icon) = hicon {
                let _ = DestroyIcon(icon);
            }
        }
    }
    Ok(())
}

/// Held in managed state so the tray's unread overlay can be updated at runtime.
/// Keeping the TrayIcon handle here also keeps the tray alive.
struct TrayUnreadState {
    tray: tauri::tray::TrayIcon,
    base_rgba: Vec<u8>,
    width: u32,
    height: u32,
}

/// Paint a small white-ringed red "unread" dot into the bottom-right corner of
/// an RGBA buffer, in place. Cross-platform (operates on raw pixels).
fn draw_unread_dot(rgba: &mut [u8], width: u32, height: u32) {
    let w = width as i32;
    let h = height as i32;
    if w <= 0 || h <= 0 {
        return;
    }
    let r = ((w.min(h) as f32) * 0.30) as i32;
    let ring = ((r as f32) * 0.18).max(1.0) as i32;
    let cx = w - r - ((w as f32) * 0.06) as i32;
    let cy = h - r - ((h as f32) * 0.06) as i32;
    for y in (cy - r - ring).max(0)..(cy + r + ring).min(h) {
        for x in (cx - r - ring).max(0)..(cx + r + ring).min(w) {
            let dx = x - cx;
            let dy = y - cy;
            let dist2 = dx * dx + dy * dy;
            let idx = ((y * w + x) as usize) * 4;
            if idx + 3 >= rgba.len() {
                continue;
            }
            if dist2 <= r * r {
                rgba[idx] = 0xDD;
                rgba[idx + 1] = 0x30;
                rgba[idx + 2] = 0x30;
                rgba[idx + 3] = 0xFF;
            } else if dist2 <= (r + ring) * (r + ring) {
                rgba[idx] = 0xFF;
                rgba[idx + 1] = 0xFF;
                rgba[idx + 2] = 0xFF;
                rgba[idx + 3] = 0xFF;
            }
        }
    }
}

/// Overlay (or clear) the unread dot on the tray icon.
#[tauri::command]
fn set_tray_unread(unread: bool, state: tauri::State<'_, TrayUnreadState>) -> Result<(), String> {
    let mut rgba = state.base_rgba.clone();
    if unread {
        draw_unread_dot(&mut rgba, state.width, state.height);
    }
    let icon = tauri::image::Image::new_owned(rgba, state.width, state.height);
    state.tray.set_icon(Some(icon)).map_err(|e| e.to_string())
}

/// Flash the taskbar button to draw attention (e.g. a new mention while the
/// window is unfocused). Clears automatically once the window is focused.
#[tauri::command]
fn flash_window(window: tauri::Window) -> Result<(), String> {
    window
        .request_user_attention(Some(tauri::UserAttentionType::Informational))
        .map_err(|e| e.to_string())
}

pub fn run() {
    let port: u16 = 44548;
    let context = tauri::generate_context!();

    #[allow(unused_mut)]
    let mut builder = tauri::Builder::default();

    // Single-instance MUST be registered first: a second launch focuses the
    // existing window (and forwards any matrix: link) instead of colliding on
    // the localhost port. Desktop-only plugin.
    #[cfg(desktop)]
    {
        builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
            show_main(app);
            if let Some(url) = matrix_url_from_args(&argv) {
                forward_deeplink(app, &url);
            }
        }));
    }

    builder = builder
        .invoke_handler(tauri::generate_handler![
            set_badge_count,
            set_tray_unread,
            flash_window,
            send_notification,
            check_for_update,
            install_update,
        ])
        .plugin(tauri_plugin_localhost::Builder::new(port).build())
        .plugin(tauri_plugin_window_state::Builder::default().build())
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_notification::init())
        .plugin(tauri_plugin_deep_link::init());

    #[cfg(not(any(target_os = "android", target_os = "ios")))]
    {
        builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
    }

    builder
        .setup(move |app| {
            // --- System tray: keeps Lotus Chat running in the background so
            // notifications keep arriving after the window is closed-to-tray. ---
            let base_icon = app.default_window_icon().cloned();
            let open_item = MenuItem::with_id(app, "open", "Open Lotus Chat", true, None::<&str>)?;
            let quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
            let separator = PredefinedMenuItem::separator(app)?;
            let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
            let tray = TrayIconBuilder::with_id("main-tray")
                .icon(base_icon.clone().expect("bundled window icon"))
                .tooltip("Lotus Chat")
                .menu(&tray_menu)
                .show_menu_on_left_click(false)
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "open" => show_main(app),
                    "quit" => app.exit(0),
                    _ => {}
                })
                .on_tray_icon_event(|tray, event| {
                    if let TrayIconEvent::Click {
                        button: MouseButton::Left,
                        button_state: MouseButtonState::Up,
                        ..
                    } = event
                    {
                        let app = tray.app_handle();
                        if let Some(window) = app.get_webview_window("main") {
                            if window.is_visible().unwrap_or(false) {
                                let _ = window.hide();
                            } else {
                                show_main(app);
                            }
                        }
                    }
                })
                .build(app)?;

            // Keep the tray handle (and base icon pixels) in managed state so
            // set_tray_unread can re-render the icon at runtime.
            if let Some(img) = base_icon {
                let base_rgba = img.rgba().to_vec();
                let (width, height) = (img.width(), img.height());
                app.manage(TrayUnreadState {
                    tray,
                    base_rgba,
                    width,
                    height,
                });
            }

            #[cfg(debug_assertions)]
            let window_url = WebviewUrl::App(Default::default());

            #[cfg(not(debug_assertions))]
            let window_url = {
                let url = format!("http://localhost:{}", port).parse().unwrap();
                WebviewUrl::External(url)
            };

            let app_handle = app.handle().clone();
            let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url)
                .title("Lotus Chat")
                // First-run defaults; tauri-plugin-window-state restores geometry
                // on later launches.
                .inner_size(1100.0, 720.0)
                .min_inner_size(480.0, 600.0)
                .center()
                // Start hidden and reveal once the page has painted, to avoid the
                // white launch flash.
                .visible(false)
                .initialization_script(NOTIFICATION_BRIDGE)
                .disable_drag_drop_handler()
                .on_page_load(|window, payload| {
                    if matches!(payload.event(), PageLoadEvent::Finished) {
                        let _ = window.show();
                    }
                })
                .on_new_window(move |url, _features| {
                    let _ = app_handle.opener().open_url(url.as_str(), None::<&str>);
                    NewWindowResponse::Deny
                })
                .build()?;

            // Close-to-tray: hide instead of exiting; the app is quit explicitly
            // from the tray menu.
            let window_for_close = window.clone();
            window.on_window_event(move |event| {
                if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                    api.prevent_close();
                    let _ = window_for_close.hide();
                }
            });

            // Failsafe: never leave the window stuck hidden if the page-load
            // event doesn't fire for some reason.
            let window_for_show = window.clone();
            std::thread::spawn(move || {
                std::thread::sleep(std::time::Duration::from_secs(8));
                let _ = window_for_show.show();
            });

            // Deep links (matrix:): route both the cold-start case and the
            // already-running case (forwarded via single-instance argv) into the
            // web client.
            {
                use tauri_plugin_deep_link::DeepLinkExt;
                // Runtime scheme registration is a Linux/Windows-only API; macOS
                // registers the scheme from the bundle config at build time.
                #[cfg(any(target_os = "linux", target_os = "windows"))]
                let _ = app.deep_link().register_all();
                let deep_link_handle = app.handle().clone();
                app.deep_link().on_open_url(move |event| {
                    for url in event.urls() {
                        forward_deeplink(&deep_link_handle, url.as_str());
                    }
                });
                if let Some(url) = matrix_url_from_args(&std::env::args().collect::<Vec<_>>()) {
                    forward_deeplink(&app.handle().clone(), &url);
                }
            }

            // Windows 11 Mica backdrop. The app paints an opaque TDS background,
            // so this is subtle (mainly window chrome); harmless if unsupported.
            #[cfg(target_os = "windows")]
            {
                let _ = window_vibrancy::apply_mica(&window, Some(true));
            }

            // Auto-grant camera, microphone, and notification permissions in WebView2.
            #[cfg(target_os = "windows")]
            window.with_webview(|webview| {
                use webview2_com::{
                    Microsoft::Web::WebView2::Win32::{
                        COREWEBVIEW2_PERMISSION_KIND,
                        COREWEBVIEW2_PERMISSION_KIND_CAMERA,
                        COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
                        COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
                        COREWEBVIEW2_PERMISSION_STATE_ALLOW,
                    },
                    PermissionRequestedEventHandler,
                };

                let controller = webview.controller();
                if let Ok(core) = unsafe { controller.CoreWebView2() } {
                    let handler = PermissionRequestedEventHandler::create(Box::new(
                        |_sender, args| {
                            if let Some(args) = args {
                                let mut kind = COREWEBVIEW2_PERMISSION_KIND(0);
                                unsafe { args.PermissionKind(&mut kind) }?;
                                if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
                                    || kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA
                                    || kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS
                                {
                                    unsafe {
                                        args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
                                    }?;
                                }
                            }
                            Ok(())
                        },
                    ));
                    let mut token = Default::default();
                    let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
                }
            })?;

            Ok(())
        })
        .run(context)
        .expect("error while building tauri application");
}
