use log::{error, info};
use axum::extract::Query;
use axum::Json;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
use crate::api_token::APIToken;
use crate::app_window::MAIN_WINDOW;

#[cfg(any(windows, target_os = "macos"))]
use std::process::Command;

#[cfg(target_os = "linux")]
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest};

#[cfg(windows)]
use std::os::windows::process::CommandExt;

/// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000.
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;

#[derive(Clone, Deserialize)]
pub struct PreviousDirectory {
    path: String,
}

#[derive(Deserialize)]
pub struct SelectDirectoryQuery {
    title: String,
}

#[derive(Clone, Deserialize)]
pub struct FileTypeFilter {
    filter_name: String,
    filter_extensions: Vec<String>,
}

#[derive(Clone, Deserialize)]
pub struct SelectFileOptions {
    title: String,
    previous_file: Option<PreviousFile>,
    filter: Option<FileTypeFilter>,
}

#[derive(Clone, Deserialize)]
pub struct SaveFileOptions {
    title: String,
    previous_file: Option<PreviousFile>,
    filter: Option<FileTypeFilter>,
}

#[derive(Clone, Deserialize)]
pub struct OpenPathOptions {
    path: String,
}

#[derive(Serialize)]
pub struct DirectorySelectionResponse {
    user_cancelled: bool,
    selected_directory: String,
}

#[derive(Serialize)]
pub struct FileSelectionResponse {
    user_cancelled: bool,
    selected_file_path: String,
}

#[derive(Serialize)]
pub struct FilesSelectionResponse {
    user_cancelled: bool,
    selected_file_paths: Vec<String>,
}

#[derive(Serialize)]
pub struct FileSaveResponse {
    user_cancelled: bool,
    save_file_path: String,
}

#[derive(Serialize)]
pub struct OpenPathResponse {
    success: bool,
    issue: String,
}

#[derive(Clone, Deserialize)]
pub struct PreviousFile {
    file_path: String,
}

/// Let the user select a directory.
pub async fn select_directory(
    _token: APIToken,
    Query(query): Query<SelectDirectoryQuery>,
    previous_directory: Option<Json<PreviousDirectory>>,
) -> Json<DirectorySelectionResponse> {
    let main_window_lock = MAIN_WINDOW.lock().unwrap();
    let main_window = match main_window_lock.as_ref() {
        Some(window) => window,
        None => {
            error!(Source = "Tauri"; "Cannot open directory dialog: main window not available.");
            return Json(DirectorySelectionResponse {
                user_cancelled: true,
                selected_directory: String::from(""),
            });
        }
    };

    let mut dialog = main_window.dialog().file().set_parent(main_window).set_title(&query.title);
    if let Some(previous) = previous_directory {
        dialog = dialog.set_directory(previous.path.clone());
    }

    drop(main_window_lock);

    let folder_path = dialog.blocking_pick_folder();
    match folder_path {
        Some(path) => {
            match path.into_path() {
                Ok(pb) => {
                    info!("User selected directory: {pb:?}");
                    Json(DirectorySelectionResponse {
                        user_cancelled: false,
                        selected_directory: pb.to_string_lossy().to_string(),
                    })
                }
                Err(e) => {
                    error!(Source = "Tauri"; "Failed to convert directory path: {e}");
                    Json(DirectorySelectionResponse {
                        user_cancelled: true,
                        selected_directory: String::new(),
                    })
                }
            }
        },

        None => {
            info!("User cancelled directory selection.");
            Json(DirectorySelectionResponse {
                user_cancelled: true,
                selected_directory: String::from(""),
            })
        },
    }
}

/// Let the user select a file.
pub async fn select_file(
    _token: APIToken,
    payload: Json<SelectFileOptions>,
) -> Json<FileSelectionResponse> {
    // Create a new file dialog builder:
    let file_dialog = MAIN_WINDOW
        .lock()
        .unwrap()
        .as_ref()
        .map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));

    let Some(mut file_dialog) = file_dialog else {
        error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
        return Json(FileSelectionResponse {
            user_cancelled: true,
            selected_file_path: String::from(""),
        });
    };

    // Set the file type filter if provided:
    file_dialog = apply_filter(file_dialog, &payload.filter);

    // Set the previous file path if provided:
    if let Some(previous) = &payload.previous_file {
        let previous_path = previous.file_path.as_str();
        file_dialog = file_dialog.set_directory(previous_path);
    }

    // Show the file dialog and get the selected file path:
    let file_path = file_dialog.blocking_pick_file();
    match file_path {
        Some(path) => match path.into_path() {
            Ok(pb) => {
                info!("User selected file: {pb:?}");
                Json(FileSelectionResponse {
                    user_cancelled: false,
                    selected_file_path: pb.to_string_lossy().to_string(),
                })
            }
            Err(e) => {
                error!(Source = "Tauri"; "Failed to convert file path: {e}");
                Json(FileSelectionResponse {
                    user_cancelled: true,
                    selected_file_path: String::new(),
                })
            }
        },

        None => {
            info!("User cancelled file selection.");
            Json(FileSelectionResponse {
                user_cancelled: true,
                selected_file_path: String::from(""),
            })
        },
    }
}

/// Let the user select some files.
pub async fn select_files(
    _token: APIToken,
    payload: Json<SelectFileOptions>,
) -> Json<FilesSelectionResponse> {
    // Create a new file dialog builder:
    let file_dialog = MAIN_WINDOW
        .lock()
        .unwrap()
        .as_ref()
        .map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));

    let Some(mut file_dialog) = file_dialog else {
        error!(Source = "Tauri"; "Cannot open file dialog: main window not available.");
        return Json(FilesSelectionResponse {
            user_cancelled: true,
            selected_file_paths: Vec::new(),
        });
    };

    // Set the file type filter if provided:
    file_dialog = apply_filter(file_dialog, &payload.filter);

    // Set the previous file path if provided:
    if let Some(previous) = &payload.previous_file {
        let previous_path = previous.file_path.as_str();
        file_dialog = file_dialog.set_directory(previous_path);
    }

    // Show the file dialog and get the selected file path:
    let file_paths = file_dialog.blocking_pick_files();
    match file_paths {
        Some(paths) => {
            let converted: Vec<String> = paths.into_iter().filter_map(|p| p.into_path().ok()).map(|pb| pb.to_string_lossy().to_string()).collect();
            info!("User selected {} files.", converted.len());
            Json(FilesSelectionResponse {
                user_cancelled: false,
                selected_file_paths: converted,
            })
        }

        None => {
            info!("User cancelled file selection.");
            Json(FilesSelectionResponse {
                user_cancelled: true,
                selected_file_paths: Vec::new(),
            })
        },
    }
}

pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
    // Create a new file dialog builder:
    let file_dialog = MAIN_WINDOW
        .lock()
        .unwrap()
        .as_ref()
        .map(|w| w.dialog().file().set_parent(w).set_title(&payload.title));

    let Some(mut file_dialog) = file_dialog else {
        error!(Source = "Tauri"; "Cannot open save dialog: main window not available.");
        return Json(FileSaveResponse {
            user_cancelled: true,
            save_file_path: String::from(""),
        });
    };

    // Set the file type filter if provided:
    file_dialog = apply_filter(file_dialog, &payload.filter);

    // Set the initial directory and file name if provided:
    if let Some(previous) = &payload.previous_file {
        let (directory, file_name) = split_save_file_path(&previous.file_path);
        if let Some(directory) = directory {
            file_dialog = file_dialog.set_directory(directory);
        }
        if let Some(file_name) = file_name {
            file_dialog = file_dialog.set_file_name(file_name);
        }
    }

    // Displays the file dialogue box and select the file:
    let file_path = file_dialog.blocking_save_file();
    match file_path {
        Some(path) => match path.into_path() {
            Ok(pb) => {
                info!("User selected file for writing operation: {pb:?}");
                Json(FileSaveResponse {
                    user_cancelled: false,
                    save_file_path: pb.to_string_lossy().to_string(),
                })
            }
            Err(e) => {
                error!(Source = "Tauri"; "Failed to convert save file path: {e}");
                Json(FileSaveResponse {
                    user_cancelled: true,
                    save_file_path: String::new(),
                })
            }
        },

        None => {
            info!("User cancelled file selection.");
            Json(FileSaveResponse {
                user_cancelled: true,
                save_file_path: String::from(""),
            })
        },
    }
}

pub async fn open_path_in_file_manager(
    _token: APIToken,
    payload: Json<OpenPathOptions>,
) -> Json<OpenPathResponse> {
    let requested_path = PathBuf::from(payload.path.trim());
    if requested_path.as_os_str().is_empty() {
        return Json(OpenPathResponse {
            success: false,
            issue: String::from("The path is empty."),
        });
    }

    match open_file_manager_target(&requested_path).await {
        Ok(()) => Json(OpenPathResponse {
            success: true,
            issue: String::new(),
        }),

        Err(issue) => {
            error!(Source = "Tauri"; "{issue}");
            Json(OpenPathResponse {
                success: false,
                issue,
            })
        }
    }
}

async fn open_file_manager_target(requested_path: &Path) -> Result<(), String> {
    let Some(target) = resolve_file_manager_target(requested_path) else {
        let issue = format!(
            "The path does not exist and its parent folder could not be found: {}",
            requested_path.to_string_lossy(),
        );
        return Err(issue);
    };

    #[cfg(target_os = "linux")]
    {
        return match open_path_in_linux_file_manager(&target).await {
            Ok(()) => {
                info!("Opened file manager for path: {:?}", target.path);
                Ok(())
            }

            Err(issue) => Err(issue),
        };
    }

    #[cfg(any(windows, target_os = "macos"))]
    {
        let mut command = create_file_manager_command(&target);

        #[cfg(windows)]
        command.creation_flags(CREATE_NO_WINDOW);

        match command.spawn() {
            Ok(_) => {
                info!("Opened file manager for path: {:?}", target.path);
                Ok(())
            }

            Err(error) => {
                let issue = format!("Failed to open the file manager: {error}");
                Err(issue)
            }
        }
    }
}

/// Applies an optional file type filter to a FileDialogBuilder.
fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> {
    match filter {
        Some(f) => file_dialog.add_filter(
            &f.filter_name,
            &f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
        ),

        None => file_dialog,
    }
}

fn split_save_file_path(file_path: &str) -> (Option<PathBuf>, Option<String>) {
    let path = Path::new(file_path);
    let directory = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
        .map(Path::to_path_buf);
        
    let file_name = path
        .file_name()
        .map(|name| name.to_string_lossy().into_owned())
        .filter(|name| !name.is_empty());

    (directory, file_name)
}

#[derive(Debug, PartialEq, Eq)]
struct FileManagerTarget {
    path: PathBuf,
    reveal_file: bool,
}

#[cfg(any(target_os = "linux", test))]
#[derive(Debug, PartialEq, Eq)]
enum LinuxPortalOperation {
    RevealFile,
    OpenDirectory,
}

fn resolve_file_manager_target(requested_path: &Path) -> Option<FileManagerTarget> {
    if requested_path.is_file() {
        return Some(FileManagerTarget {
            path: requested_path.to_path_buf(),
            reveal_file: true,
        });
    }

    if requested_path.is_dir() {
        return Some(FileManagerTarget {
            path: requested_path.to_path_buf(),
            reveal_file: false,
        });
    }

    requested_path.parent()
        .filter(|parent| parent.is_dir())
        .map(|parent| FileManagerTarget {
            path: parent.to_path_buf(),
            reveal_file: false,
        })
}

#[cfg(any(target_os = "linux", test))]
fn linux_portal_operation(target: &FileManagerTarget) -> LinuxPortalOperation {
    if target.reveal_file {
        LinuxPortalOperation::RevealFile
    } else {
        LinuxPortalOperation::OpenDirectory
    }
}

#[cfg(any(target_os = "linux", test))]
fn xdg_open_fallback_path(target: &FileManagerTarget) -> &Path {
    if target.reveal_file {
        target.path.parent().unwrap_or(&target.path)
    } else {
        &target.path
    }
}

#[cfg(target_os = "linux")]
enum LinuxPortalError {
    Unavailable(String),
    RequestFailed(String),
}

#[cfg(target_os = "linux")]
async fn open_path_with_linux_portal(target: &FileManagerTarget) -> Result<(), LinuxPortalError> {
    let file = std::fs::File::open(&target.path)
        .map_err(|error| LinuxPortalError::Unavailable(format!("Failed to open the path for the desktop portal: {error}")))?;

    let request = match linux_portal_operation(target) {
        LinuxPortalOperation::RevealFile => OpenDirectoryRequest::default().send(&file).await,
        LinuxPortalOperation::OpenDirectory => OpenFileRequest::default().send_file(&file).await,
    }
    .map_err(|error| LinuxPortalError::Unavailable(format!("Desktop portal invocation failed: {error}")))?;

    request.response()
        .map_err(|error| LinuxPortalError::RequestFailed(format!("Desktop portal request failed: {error}")))
}

#[cfg(target_os = "linux")]
async fn open_path_with_xdg_open(target: &FileManagerTarget) -> Result<(), String> {
    let fallback_path = xdg_open_fallback_path(target);
    let status = tokio::process::Command::new("xdg-open")
        .arg(fallback_path)
        .status()
        .await
        .map_err(|error| format!("xdg-open failed to start for '{}': {error}", fallback_path.to_string_lossy()))?;

    if status.success() {
        Ok(())
    } else {
        Err(format!("xdg-open failed for '{}' with exit status {status}", fallback_path.to_string_lossy()))
    }
}

#[cfg(target_os = "linux")]
async fn open_path_in_linux_file_manager(target: &FileManagerTarget) -> Result<(), String> {
    match open_path_with_linux_portal(target).await {
        Ok(()) => Ok(()),
        Err(LinuxPortalError::RequestFailed(error)) => Err(error),
        Err(LinuxPortalError::Unavailable(portal_error)) => {
            match open_path_with_xdg_open(target).await {
                Ok(()) => Ok(()),
                Err(fallback_error) => Err(format!("{portal_error} Fallback failed: {fallback_error}")),
            }
        }
    }
}

#[cfg(target_os = "windows")]
fn create_file_manager_command(target: &FileManagerTarget) -> Command {
    let mut command = Command::new("explorer.exe");
    if target.reveal_file {
        command.arg(format!("/select,{}", target.path.to_string_lossy()));
    } else {
        command.arg(&target.path);
    }

    command
}

#[cfg(target_os = "macos")]
fn create_file_manager_command(target: &FileManagerTarget) -> Command {
    let mut command = Command::new("open");
    if target.reveal_file {
        command.arg("-R");
    }

    command.arg(&target.path);
    command
}

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

    #[test]
    fn save_file_options_accept_the_previous_file_contract() {
        let options: SaveFileOptions = serde_json::from_str(
            r#"{"title":"Export visual briefing","previous_file":{"file_path":"Quarterly briefing.html"}}"#,
        )
        .unwrap();

        assert_eq!(options.title, "Export visual briefing");
        assert_eq!(
            options.previous_file.unwrap().file_path,
            "Quarterly briefing.html",
        );
    }

    #[test]
    fn save_file_name_without_directory_is_preserved() {
        let (directory, file_name) = split_save_file_path("Quarterly briefing.html");

        assert_eq!(directory, None);
        assert_eq!(file_name.as_deref(), Some("Quarterly briefing.html"));
    }

    #[test]
    fn save_file_path_is_split_into_directory_and_name() {
        let temp_dir = tempfile::tempdir().unwrap();
        let initial_path = temp_dir.path().join("Quarterly briefing.html");

        let (directory, file_name) = split_save_file_path(initial_path.to_str().unwrap());

        assert_eq!(directory.as_deref(), Some(temp_dir.path()));
        assert_eq!(file_name.as_deref(), Some("Quarterly briefing.html"));
    }

    #[test]
    fn existing_file_is_revealed_and_falls_back_to_its_parent() {
        let temp_dir = tempfile::tempdir().unwrap();
        let file_path = temp_dir.path().join("application.log");
        fs::write(&file_path, "log").unwrap();

        let target = resolve_file_manager_target(&file_path).unwrap();

        assert_eq!(target.path, file_path);
        assert!(target.reveal_file);
        assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::RevealFile);
        assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
    }

    #[test]
    fn existing_directory_is_opened_directly() {
        let temp_dir = tempfile::tempdir().unwrap();

        let target = resolve_file_manager_target(temp_dir.path()).unwrap();

        assert_eq!(target.path, temp_dir.path());
        assert!(!target.reveal_file);
        assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
        assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
    }

    #[test]
    fn missing_file_uses_its_existing_parent_directory() {
        let temp_dir = tempfile::tempdir().unwrap();
        let missing_file = temp_dir.path().join("missing.log");

        let target = resolve_file_manager_target(&missing_file).unwrap();

        assert_eq!(target.path, temp_dir.path());
        assert!(!target.reveal_file);
        assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
        assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
    }

    #[test]
    fn invalid_path_without_existing_parent_is_rejected() {
        let temp_dir = tempfile::tempdir().unwrap();
        let invalid_path = temp_dir.path().join("missing-directory").join("missing.log");

        assert!(resolve_file_manager_target(&invalid_path).is_none());
    }
}