use crate::storage;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::SystemTime;
#[cfg(not(target_os = "windows"))]
use tauri_plugin_opener::OpenerExt;

const FALLBACK_ICON_DATA_URL: &str = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3E%3Crect%20x='5'%20y='5'%20width='22'%20height='22'%20rx='5'%20fill='%23eef3fb'%20stroke='%2395a3b8'/%3E%3Cpath%20d='M11%2012h10M11%2016h10M11%2020h6'%20stroke='%23516275'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E";

#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AppLauncherLaunchMode {
    Normal,
    Admin,
    DifferentUser,
    OpenFolder,
}

#[derive(Debug, PartialEq, Eq)]
pub struct AppLauncherLaunchPlan {
    pub target: String,
    pub parameters: Option<String>,
    pub working_directory: Option<String>,
    pub operation: Option<&'static str>,
}

#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PrepareAppLauncherEntryRequest {
    path: String,
}

#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LaunchAppLauncherEntryRequest {
    path: String,
    arguments: Option<String>,
    working_directory: Option<String>,
    mode: AppLauncherLaunchMode,
}

pub fn prepare_entry(request: PrepareAppLauncherEntryRequest) -> PreparedAppLauncherEntry {
    let path = request.path.trim().to_string();
    let metadata = std::fs::metadata(&path).ok();
    let exists = metadata.is_some();
    let file_kind = match metadata.as_ref() {
        Some(metadata) if metadata.is_dir() => AppLauncherFileKind::Folder,
        Some(_) => AppLauncherFileKind::File,
        None => AppLauncherFileKind::Missing,
    };
    PreparedAppLauncherEntry {
        name: storage::app_launcher_name_from_path(&path),
        exists,
        runnable: is_runnable_path(&path),
        icon_data_url: icon_data_url_for_path(&path),
        extension: path_extension(&path),
        size_bytes: metadata
            .as_ref()
            .filter(|metadata| metadata.is_file())
            .map(|metadata| metadata.len()),
        modified_at_unix_ms: metadata
            .and_then(|metadata| metadata.modified().ok())
            .and_then(system_time_to_unix_ms),
        file_kind,
        path,
    }
}

pub fn launch_entry(
    app: tauri::AppHandle,
    request: LaunchAppLauncherEntryRequest,
) -> Result<(), String> {
    let plan = plan_launch_with_options(
        &request.path,
        request.arguments.as_deref(),
        request.working_directory.as_deref(),
        request.mode,
    )?;
    launch_plan(app, plan)
}

#[cfg(test)]
pub fn plan_launch(
    path: &str,
    mode: AppLauncherLaunchMode,
) -> Result<AppLauncherLaunchPlan, String> {
    plan_launch_with_options(path, None, None, mode)
}

fn plan_launch_with_options(
    path: &str,
    arguments: Option<&str>,
    working_directory: Option<&str>,
    mode: AppLauncherLaunchMode,
) -> Result<AppLauncherLaunchPlan, String> {
    let path = path.trim();
    if path.is_empty() {
        return Err("App Launcher path is required".to_string());
    }

    if mode == AppLauncherLaunchMode::OpenFolder {
        let target_folder = containing_folder_for_launcher_path(path)?;
        return Ok(AppLauncherLaunchPlan {
            target: target_folder,
            parameters: None,
            working_directory: None,
            operation: None,
        });
    }

    let runnable = is_runnable_path(path);
    if mode != AppLauncherLaunchMode::Normal && !runnable {
        return Err(
            "Admin and alternate-user launch are only available for runnable files".to_string(),
        );
    }

    let operation = match mode {
        AppLauncherLaunchMode::Normal => None,
        AppLauncherLaunchMode::Admin => Some("runas"),
        AppLauncherLaunchMode::DifferentUser => Some("runasuser"),
        AppLauncherLaunchMode::OpenFolder => None,
    };
    let arguments = arguments.map(str::trim).filter(|value| !value.is_empty());
    let working_directory = working_directory
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned);

    if is_powershell_script(path) {
        let parameters = Some(match arguments {
            Some(arguments) => format!("-File \"{}\" {arguments}", path.replace('"', "\\\"")),
            None => format!("-File \"{}\"", path.replace('"', "\\\"")),
        });
        return Ok(AppLauncherLaunchPlan {
            target: "powershell.exe".to_string(),
            parameters,
            working_directory,
            operation,
        });
    }

    if mode == AppLauncherLaunchMode::Normal && !runnable {
        return Ok(AppLauncherLaunchPlan {
            target: "explorer.exe".to_string(),
            parameters: Some(path.to_string()),
            working_directory,
            operation,
        });
    }

    Ok(AppLauncherLaunchPlan {
        target: path.to_string(),
        parameters: arguments.map(ToOwned::to_owned),
        working_directory,
        operation,
    })
}

fn launch_plan(app: tauri::AppHandle, plan: AppLauncherLaunchPlan) -> Result<(), String> {
    #[cfg(target_os = "windows")]
    {
        let _ = app;
        launch_plan_windows(plan)
    }

    #[cfg(not(target_os = "windows"))]
    {
        if plan.operation.is_some() {
            return Err(
                "Admin and alternate-user launch are only available on Windows".to_string(),
            );
        }
        if plan.parameters.is_none() && plan.working_directory.is_none() {
            return app
                .opener()
                .open_path(plan.target, None::<&str>)
                .map_err(|error| format!("failed to open launcher entry: {error}"));
        }
        let mut command = std::process::Command::new(&plan.target);
        if let Some(parameters) = plan.parameters {
            command.arg(parameters);
        }
        if let Some(working_directory) = plan.working_directory {
            command.current_dir(working_directory);
        }
        command
            .spawn()
            .map(|_| ())
            .map_err(|error| format!("failed to launch {}: {error}", plan.target))
    }
}

#[cfg(target_os = "windows")]
fn launch_plan_windows(plan: AppLauncherLaunchPlan) -> Result<(), String> {
    use std::ptr::{null, null_mut};
    use windows_sys::Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOWNORMAL};

    if plan.operation.is_none() && plan.target.eq_ignore_ascii_case("explorer.exe") {
        let mut command = std::process::Command::new(&plan.target);
        if let Some(parameters) = plan.parameters.as_deref() {
            command.arg(parameters);
        }
        if let Some(working_directory) = plan.working_directory.as_deref() {
            command.current_dir(working_directory);
        }
        return command
            .spawn()
            .map(|_| ())
            .map_err(|error| format!("failed to launch {}: {error}", plan.target));
    }

    let operation = plan.operation.map(wide_string);
    let target = wide_string(&plan.target);
    let parameters = plan.parameters.as_ref().map(|value| wide_string(value));
    let working_directory = plan
        .working_directory
        .as_ref()
        .map(|value| wide_string(value));
    let result = unsafe {
        ShellExecuteW(
            null_mut(),
            operation
                .as_ref()
                .map(|value| value.as_ptr())
                .unwrap_or(null()),
            target.as_ptr(),
            parameters
                .as_ref()
                .map(|value| value.as_ptr())
                .unwrap_or(null()),
            working_directory
                .as_ref()
                .map(|value| value.as_ptr())
                .unwrap_or(null()),
            SW_SHOWNORMAL,
        )
    } as isize;

    if result <= 32 {
        return Err(format!("failed to launch {}", plan.target));
    }

    Ok(())
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PreparedAppLauncherEntry {
    pub name: String,
    pub path: String,
    pub exists: bool,
    pub runnable: bool,
    pub icon_data_url: Option<String>,
    pub file_kind: AppLauncherFileKind,
    pub extension: Option<String>,
    pub size_bytes: Option<u64>,
    pub modified_at_unix_ms: Option<u64>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub enum AppLauncherFileKind {
    File,
    Folder,
    Missing,
}

fn icon_data_url_for_path(path: &str) -> Option<String> {
    native_icon_data_url(path).or_else(|| Some(FALLBACK_ICON_DATA_URL.to_string()))
}

#[cfg(target_os = "windows")]
fn native_icon_data_url(path: &str) -> Option<String> {
    high_quality_shell_icon(path)
        .or_else(|| legacy_shell_icon(path))
        .and_then(|(icon, size)| icon_handle_to_data_url(icon, size))
}

#[cfg(target_os = "windows")]
type WindowsIconHandle = windows_sys::Win32::UI::WindowsAndMessaging::HICON;

#[cfg(target_os = "windows")]
fn high_quality_shell_icon(path: &str) -> Option<(WindowsIconHandle, i32)> {
    use std::ffi::c_void;
    use std::mem::{size_of, zeroed};
    use windows_sys::Win32::UI::Controls::{
        ILD_TRANSPARENT, ImageList_GetIcon, ImageList_GetIconSize,
    };
    use windows_sys::Win32::UI::Shell::{
        IUnknown_AtomicRelease, SHFILEINFOW, SHGFI_SYSICONINDEX, SHGetFileInfoW, SHGetImageList,
        SHIL_EXTRALARGE, SHIL_JUMBO,
    };

    const IID_IIMAGELIST: windows_sys::core::GUID =
        windows_sys::core::GUID::from_u128(0x46eb5926_582e_4017_9fdf_e8998daa0950);

    let wide_path = wide_string(path);
    let mut shell_info: SHFILEINFOW = unsafe { zeroed() };
    let info_result = unsafe {
        SHGetFileInfoW(
            wide_path.as_ptr(),
            0,
            &mut shell_info,
            size_of::<SHFILEINFOW>() as u32,
            SHGFI_SYSICONINDEX,
        )
    };
    if info_result == 0 || shell_info.iIcon < 0 {
        return None;
    }

    for image_list_kind in [SHIL_JUMBO, SHIL_EXTRALARGE] {
        let mut image_list: *mut c_void = std::ptr::null_mut();
        let result =
            unsafe { SHGetImageList(image_list_kind as i32, &IID_IIMAGELIST, &mut image_list) };
        if result < 0 || image_list.is_null() {
            continue;
        }

        let image_list_handle = image_list as isize;
        let icon =
            unsafe { ImageList_GetIcon(image_list_handle, shell_info.iIcon, ILD_TRANSPARENT) };
        let mut width = 0;
        let mut height = 0;
        let has_size =
            unsafe { ImageList_GetIconSize(image_list_handle, &mut width, &mut height) } != 0;
        unsafe {
            IUnknown_AtomicRelease(&mut image_list);
        }
        if icon.is_null() {
            continue;
        }
        let fallback_size = if image_list_kind == SHIL_JUMBO {
            256
        } else {
            48
        };
        let icon_size = if has_size {
            width.max(height).clamp(32, 256)
        } else {
            fallback_size
        };
        return Some((icon, icon_size));
    }

    None
}

#[cfg(target_os = "windows")]
fn legacy_shell_icon(path: &str) -> Option<(WindowsIconHandle, i32)> {
    use std::mem::{size_of, zeroed};
    use windows_sys::Win32::UI::Shell::{SHFILEINFOW, SHGFI_ICON, SHGFI_LARGEICON, SHGetFileInfoW};

    let wide_path = wide_string(path);
    let mut shell_info: SHFILEINFOW = unsafe { zeroed() };
    let info_result = unsafe {
        SHGetFileInfoW(
            wide_path.as_ptr(),
            0,
            &mut shell_info,
            size_of::<SHFILEINFOW>() as u32,
            SHGFI_ICON | SHGFI_LARGEICON,
        )
    };
    if info_result == 0 || shell_info.hIcon.is_null() {
        return None;
    }

    Some((shell_info.hIcon, 32))
}

#[cfg(target_os = "windows")]
fn icon_handle_to_data_url(icon: WindowsIconHandle, icon_size: i32) -> Option<String> {
    use base64::{Engine as _, engine::general_purpose::STANDARD};
    use image::{ColorType, ImageEncoder, codecs::png::PngEncoder};
    use std::ffi::c_void;
    use std::mem::{size_of, zeroed};
    use std::ptr::null_mut;
    use windows_sys::Win32::Graphics::Gdi::{
        BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, CreateCompatibleDC,
        DIB_RGB_COLORS, DeleteDC, DeleteObject, GetDC, GetDIBits, ReleaseDC, SelectObject,
    };
    use windows_sys::Win32::UI::WindowsAndMessaging::{DI_NORMAL, DestroyIcon, DrawIconEx};

    if icon.is_null() || icon_size <= 0 {
        return None;
    }

    let screen_hdc = unsafe { GetDC(null_mut()) };
    if screen_hdc.is_null() {
        unsafe {
            DestroyIcon(icon);
        }
        return None;
    }
    let hdc = unsafe { CreateCompatibleDC(screen_hdc) };
    if hdc.is_null() {
        unsafe {
            ReleaseDC(null_mut(), screen_hdc);
            DestroyIcon(icon);
        }
        return None;
    }
    let bitmap = unsafe { CreateCompatibleBitmap(screen_hdc, icon_size, icon_size) };
    if bitmap.is_null() {
        unsafe {
            DeleteDC(hdc);
            ReleaseDC(null_mut(), screen_hdc);
            DestroyIcon(icon);
        }
        return None;
    }
    let previous = unsafe { SelectObject(hdc, bitmap) };
    let drawn = unsafe {
        DrawIconEx(
            hdc,
            0,
            0,
            icon,
            icon_size,
            icon_size,
            0,
            null_mut(),
            DI_NORMAL,
        )
    };
    if drawn == 0 {
        unsafe {
            SelectObject(hdc, previous);
            DeleteObject(bitmap);
            DeleteDC(hdc);
            ReleaseDC(null_mut(), screen_hdc);
            DestroyIcon(icon);
        }
        return None;
    }

    let mut bitmap_info = BITMAPINFO {
        bmiHeader: BITMAPINFOHEADER {
            biSize: size_of::<BITMAPINFOHEADER>() as u32,
            biWidth: icon_size,
            biHeight: -icon_size,
            biPlanes: 1,
            biBitCount: 32,
            biCompression: BI_RGB,
            biSizeImage: 0,
            biXPelsPerMeter: 0,
            biYPelsPerMeter: 0,
            biClrUsed: 0,
            biClrImportant: 0,
        },
        bmiColors: [unsafe { zeroed() }],
    };
    let mut bgra = vec![0u8; (icon_size * icon_size * 4) as usize];
    let read = unsafe {
        GetDIBits(
            hdc,
            bitmap,
            0,
            icon_size as u32,
            bgra.as_mut_ptr().cast::<c_void>(),
            &mut bitmap_info,
            DIB_RGB_COLORS,
        )
    };

    unsafe {
        SelectObject(hdc, previous);
        DeleteObject(bitmap);
        DeleteDC(hdc);
        ReleaseDC(null_mut(), screen_hdc);
        DestroyIcon(icon);
    }

    if read == 0 {
        return None;
    }

    let mut rgba = bgra;
    for pixel in rgba.chunks_exact_mut(4) {
        pixel.swap(0, 2);
        if pixel[3] == 0 && (pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0) {
            pixel[3] = 255;
        }
    }
    let (rgba, png_width, png_height) =
        trim_icon_transparent_padding(&rgba, icon_size as u32, icon_size as u32)?;
    let mut png = Vec::new();
    PngEncoder::new(&mut png)
        .write_image(&rgba, png_width, png_height, ColorType::Rgba8.into())
        .ok()?;
    Some(format!("data:image/png;base64,{}", STANDARD.encode(png)))
}

#[cfg(not(target_os = "windows"))]
fn native_icon_data_url(_path: &str) -> Option<String> {
    None
}

fn is_runnable_path(path: &str) -> bool {
    matches!(
        path_extension(path).as_deref(),
        Some("exe" | "lnk" | "bat" | "cmd" | "ps1")
    )
}

fn is_powershell_script(path: &str) -> bool {
    path_extension(path).as_deref() == Some("ps1")
}

fn path_extension(path: &str) -> Option<String> {
    Path::new(path)
        .extension()
        .and_then(|extension| extension.to_str())
        .map(|extension| extension.to_lowercase())
}

fn containing_folder_for_launcher_path(path: &str) -> Result<String, String> {
    let path = Path::new(path);
    if path.is_dir() {
        return Ok(path.to_string_lossy().to_string());
    }
    path.parent()
        .map(|parent| parent.to_string_lossy().to_string())
        .filter(|parent| !parent.trim().is_empty())
        .ok_or_else(|| "Could not determine the launcher entry folder".to_string())
}

fn system_time_to_unix_ms(time: SystemTime) -> Option<u64> {
    let millis = time
        .duration_since(SystemTime::UNIX_EPOCH)
        .ok()?
        .as_millis();
    u64::try_from(millis).ok()
}

#[cfg(test)]
fn rgba_has_visible_pixels(rgba: &[u8]) -> bool {
    rgba.chunks_exact(4).any(icon_pixel_is_visible)
}

#[cfg(any(target_os = "windows", test))]
fn trim_icon_transparent_padding(
    rgba: &[u8],
    width: u32,
    height: u32,
) -> Option<(Vec<u8>, u32, u32)> {
    if width == 0 || height == 0 || rgba.len() != (width as usize * height as usize * 4) {
        return None;
    }

    let mut min_x = width;
    let mut min_y = height;
    let mut max_x = 0;
    let mut max_y = 0;
    let mut has_visible_pixel = false;

    for y in 0..height {
        for x in 0..width {
            let index = ((y * width + x) * 4) as usize;
            if icon_pixel_is_visible(&rgba[index..index + 4]) {
                min_x = min_x.min(x);
                min_y = min_y.min(y);
                max_x = max_x.max(x);
                max_y = max_y.max(y);
                has_visible_pixel = true;
            }
        }
    }

    if !has_visible_pixel {
        return None;
    }

    let cropped_width = max_x - min_x + 1;
    let cropped_height = max_y - min_y + 1;
    let mut cropped = Vec::with_capacity((cropped_width * cropped_height * 4) as usize);
    for y in min_y..=max_y {
        let start = ((y * width + min_x) * 4) as usize;
        let end = start + (cropped_width * 4) as usize;
        cropped.extend_from_slice(&rgba[start..end]);
    }

    Some((cropped, cropped_width, cropped_height))
}

#[cfg(any(target_os = "windows", test))]
fn icon_pixel_is_visible(pixel: &[u8]) -> bool {
    pixel[3] > 0 && (pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0)
}

#[cfg(target_os = "windows")]
fn wide_string(value: &str) -> Vec<u16> {
    value.encode_utf16().chain(std::iter::once(0)).collect()
}

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

    #[test]
    fn launch_plan_allows_normal_for_arbitrary_files() {
        let plan = plan_launch("C:\\Docs\\notes.txt", AppLauncherLaunchMode::Normal)
            .expect("normal launches can open associated files");

        assert_eq!(plan.target, "explorer.exe");
        assert_eq!(plan.parameters.as_deref(), Some("C:\\Docs\\notes.txt"));
        assert_eq!(plan.operation, None);
    }

    #[test]
    fn launch_plan_opens_office_documents_through_explorer() {
        let plan = plan_launch("C:\\Docs\\budget.xlsx", AppLauncherLaunchMode::Normal)
            .expect("office documents open through their shell association");

        assert_eq!(plan.target, "explorer.exe");
        assert_eq!(plan.parameters.as_deref(), Some("C:\\Docs\\budget.xlsx"));
        assert_eq!(plan.operation, None);
    }

    #[test]
    fn launch_plan_opens_folders_through_explorer() {
        let plan = plan_launch(
            "C:\\Users\\example\\Documents",
            AppLauncherLaunchMode::Normal,
        )
        .expect("folders open in File Explorer");

        assert_eq!(plan.target, "explorer.exe");
        assert_eq!(
            plan.parameters.as_deref(),
            Some("C:\\Users\\example\\Documents")
        );
        assert_eq!(plan.operation, None);
    }

    #[test]
    fn launch_plan_limits_admin_to_runnable_files() {
        let plan = plan_launch("C:\\Tools\\script.ps1", AppLauncherLaunchMode::Admin)
            .expect("scripts can use admin launch");

        assert_eq!(plan.target, "powershell.exe");
        assert_eq!(plan.operation, Some("runas"));

        let error = plan_launch("C:\\Docs\\notes.txt", AppLauncherLaunchMode::Admin)
            .expect_err("documents cannot use admin launch");
        assert!(error.contains("only available for runnable files"));
    }

    #[test]
    fn launch_plan_uses_windows_runasuser_verb_for_alternate_user() {
        let plan = plan_launch("C:\\Tools\\tool.exe", AppLauncherLaunchMode::DifferentUser)
            .expect("executables can use alternate-user launch");

        assert_eq!(plan.target, "C:\\Tools\\tool.exe");
        assert_eq!(plan.operation, Some("runasuser"));
    }

    #[test]
    fn launch_plan_open_folder_uses_containing_folder() {
        let plan = plan_launch("C:\\Tools\\tool.exe", AppLauncherLaunchMode::OpenFolder)
            .expect("open folder should target the parent folder");

        assert_eq!(plan.target, "C:\\Tools");
        assert_eq!(plan.parameters, None);
        assert_eq!(plan.operation, None);
    }

    #[test]
    fn visible_icon_pixels_reject_all_transparent_images() {
        assert!(!rgba_has_visible_pixels(&[0, 0, 0, 0, 0, 0, 0, 0]));
        assert!(rgba_has_visible_pixels(&[0, 0, 0, 0, 12, 34, 56, 255]));
    }

    #[test]
    fn icon_bitmap_trimming_removes_transparent_padding() {
        let rgba = [
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //
            0, 0, 0, 0, 20, 40, 60, 255, 21, 41, 61, 255, //
            0, 0, 0, 0, 22, 42, 62, 255, 23, 43, 63, 255, //
        ];

        let (trimmed, width, height) =
            trim_icon_transparent_padding(&rgba, 3, 3).expect("visible icon pixels");

        assert_eq!((width, height), (2, 2));
        assert_eq!(
            trimmed,
            vec![
                20, 40, 60, 255, 21, 41, 61, 255, //
                22, 42, 62, 255, 23, 43, 63, 255,
            ]
        );
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn native_icon_data_url_extracts_visible_windows_app_icon() {
        let system_root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".to_string());
        let notepad = std::path::Path::new(&system_root)
            .join("System32")
            .join("notepad.exe");
        if !notepad.exists() {
            return;
        }

        let icon_data_url = native_icon_data_url(&notepad.to_string_lossy())
            .expect("notepad icon should be extractable");

        assert!(icon_data_url.starts_with("data:image/png;base64,"));
    }
}
