// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use notify::{RecursiveMode, Watcher};
use opendal::services::{S3, Webdav};
use opendal::{EntryMode, ErrorKind, Operator};
use std::collections::{BTreeMap, BTreeSet};
use std::future::{Future, IntoFuture};
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
use tauri::{Emitter, Manager};
use tauri_plugin_dialog::DialogExt;
use base64::Engine;
use regex::Regex;

const SYNC_CHECK_TIMEOUT: Duration = Duration::from_secs(15);
const SYNC_TEST_STEP_TIMEOUT: Duration = Duration::from_secs(15);
const SYNC_REMOTE_LIST_TIMEOUT: Duration = Duration::from_secs(120);
const SYNC_REMOTE_FILE_TIMEOUT: Duration = Duration::from_secs(180);
const SYNC_TEST_PAYLOAD: &[u8] = b"yurou sync connection probe\n";

#[derive(Debug, Clone, serde::Serialize)]
struct DirEntry {
    name: String,
    path: String,
    is_dir: bool,
}

#[derive(Default)]
struct WorkspaceWatcherState {
    watcher: Mutex<Option<notify::RecommendedWatcher>>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct WorkspaceChangedPayload {
    root: String,
    paths: Vec<String>,
    kind: String,
}

#[derive(Debug, Clone, serde::Serialize)]
struct WorkspaceWatchErrorPayload {
    root: String,
    message: String,
}

#[derive(Debug, Clone, serde::Serialize)]
struct SearchMatch {
    line: usize,
    column: usize,
    line_text: String,
    context_before: Vec<String>,
    context_after: Vec<String>,
}

#[derive(Debug, Clone, serde::Serialize)]
struct FileSearchResult {
    relative_path: String,
    absolute_path: String,
    matches: Vec<SearchMatch>,
    match_count: usize,
}

fn is_hidden_entry(entry: &fs::DirEntry) -> bool {
    let name = entry.file_name();
    let is_dot_hidden = name.to_string_lossy().starts_with('.');

    #[cfg(windows)]
    {
        use std::os::windows::fs::MetadataExt;

        const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2;
        let is_windows_hidden = entry
            .metadata()
            .map(|metadata| metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0)
            .unwrap_or(false);

        return is_dot_hidden || is_windows_hidden;
    }

    #[cfg(not(windows))]
    {
        is_dot_hidden
    }
}

/// Returns the companion hidden resources directory path for a markdown file.
/// Example: `notes/doc.md` → `notes/.doc`
fn resources_dir_path(file_path: &str) -> Option<String> {
    let path = std::path::Path::new(file_path);
    if path.extension().map_or(true, |ext| ext != "md") {
        return None;
    }
    let stem = path.file_stem()?.to_str()?;
    let parent = path.parent()?;
    let dir_name = format!(".{}", stem);
    Some(parent.join(dir_name).to_string_lossy().to_string())
}

/// On Windows, set the hidden attribute on a directory so it doesn't clutter
/// the file manager.
#[cfg(windows)]
fn hide_windows_dir(dir_path: &str) {
    let _ = std::process::Command::new("attrib")
        .args(["+h", dir_path])
        .output();
}

/// Ensure a directory exists, and hide it on Windows when the name starts with `.`.
fn ensure_resources_dir(dir_path: &str) -> std::io::Result<()> {
    fs::create_dir_all(dir_path)?;
    #[cfg(windows)]
    {
        let name = std::path::Path::new(dir_path)
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("");
        if name.starts_with('.') {
            hide_windows_dir(dir_path);
        }
    }
    Ok(())
}

#[tauri::command]
fn save_media_file(path: String, data: String) -> Result<(), String> {
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(&data)
        .map_err(|e| format!("解码失败: {}", e))?;

    if let Some(parent) = std::path::Path::new(&path).parent() {
        let parent_str = parent.to_string_lossy().to_string();
        ensure_resources_dir(&parent_str)
            .map_err(|e| format!("无法创建目录: {}", e))?;
    }

    fs::write(&path, &bytes).map_err(|e| format!("无法写入文件: {}", e))
}

#[tauri::command]
fn pick_folder(app: tauri::AppHandle) -> Option<String> {
    app.dialog()
        .file()
        .blocking_pick_folder()
        .map(|p| p.to_string())
}

#[tauri::command]
fn read_dir(app: tauri::AppHandle, path: String) -> Result<Vec<DirEntry>, String> {
    let entries = fs::read_dir(&path).map_err(|e| format!("无法读取目录: {}", e))?;
    app.asset_protocol_scope()
        .allow_directory(&path, true)
        .map_err(|e| format!("无法授权媒体预览目录: {}", e))?;

    let allowed_extensions = [
        "md", "png", "jpg", "jpeg", "gif", "webp", "avif", "mp4", "webm", "mov", "m4v", "ogv",
    ];
    let mut result = Vec::new();
    for entry in entries {
        let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?;
        if is_hidden_entry(&entry) {
            continue;
        }

        let path = entry.path();
        let is_dir = path.is_dir();
        // Keep the workspace tree focused on markdown documents and embeddable media.
        if !is_dir {
            let ext = path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("")
                .to_lowercase();
            if !allowed_extensions.contains(&ext.as_str()) {
                continue;
            }
        }
        let name = entry.file_name().to_string_lossy().to_string();
        result.push(DirEntry {
            name,
            path: path.to_string_lossy().to_string(),
            is_dir,
        });
    }
    // Sort: directories first, then alphabetical
    result.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then_with(|| a.name.cmp(&b.name)));
    Ok(result)
}

#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    fs::read_to_string(&path).map_err(|e| format!("无法读取文件: {}", e))
}

#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
    fs::write(&path, &content).map_err(|e| format!("无法保存文件: {}", e))
}

#[tauri::command]
fn watch_workspace(
    app: tauri::AppHandle,
    state: tauri::State<'_, WorkspaceWatcherState>,
    path: String,
) -> Result<(), String> {
    let watch_path = PathBuf::from(&path);
    if !watch_path.is_dir() {
        return Err("工作目录不存在或不是文件夹".to_string());
    }

    let root = watch_path.to_string_lossy().to_string();
    let event_root = root.clone();
    let error_root = root.clone();
    let app_handle = app.clone();
    let mut watcher =
        notify::recommended_watcher(move |result: notify::Result<notify::Event>| match result {
            Ok(event) => {
                if event.kind.is_access() {
                    return;
                }

                let payload = WorkspaceChangedPayload {
                    root: event_root.clone(),
                    paths: event
                        .paths
                        .iter()
                        .map(|path| path.to_string_lossy().to_string())
                        .collect(),
                    kind: format!("{:?}", event.kind),
                };
                let _ = app_handle.emit("workspace-changed", payload);
            }
            Err(error) => {
                let _ = app_handle.emit(
                    "workspace-watch-error",
                    WorkspaceWatchErrorPayload {
                        root: error_root.clone(),
                        message: error.to_string(),
                    },
                );
            }
        })
        .map_err(|e| format!("无法监听工作目录: {}", e))?;

    watcher
        .watch(&watch_path, RecursiveMode::Recursive)
        .map_err(|e| format!("无法监听工作目录: {}", e))?;

    let mut current = state
        .watcher
        .lock()
        .map_err(|_| "无法更新工作目录监听状态".to_string())?;
    *current = Some(watcher);
    Ok(())
}

#[tauri::command]
fn unwatch_workspace(state: tauri::State<'_, WorkspaceWatcherState>) -> Result<(), String> {
    let mut current = state
        .watcher
        .lock()
        .map_err(|_| "无法更新工作目录监听状态".to_string())?;
    *current = None;
    Ok(())
}

#[tauri::command]
fn pick_save_file(app: tauri::AppHandle) -> Option<String> {
    app.dialog()
        .file()
        .blocking_save_file()
        .map(|p| p.to_string())
}

#[tauri::command]
fn create_folder(path: String) -> Result<(), String> {
    fs::create_dir_all(&path).map_err(|e| format!("无法创建文件夹: {}", e))
}

#[tauri::command]
fn create_file(path: String) -> Result<(), String> {
    if let Some(parent) = PathBuf::from(&path).parent() {
        fs::create_dir_all(parent).map_err(|e| format!("无法创建父目录: {}", e))?;
    }
    fs::write(&path, "").map_err(|e| format!("无法创建文件: {}", e))?;

    // Create companion hidden resources directory for markdown files.
    if let Some(res_dir) = resources_dir_path(&path) {
        ensure_resources_dir(&res_dir).map_err(|e| format!("无法创建资源文件夹: {}", e))?;
    }

    Ok(())
}

#[tauri::command]
fn delete_entry(path: String) -> Result<(), String> {
    let p = PathBuf::from(&path);
    if p.is_dir() {
        fs::remove_dir_all(&p).map_err(|e| format!("无法删除文件夹: {}", e))
    } else if p.is_file() {
        fs::remove_file(&p).map_err(|e| format!("无法删除文件: {}", e))?;

        // If this was a markdown file, also remove its companion resources directory.
        if let Some(res_dir) = resources_dir_path(&path) {
            let res = PathBuf::from(&res_dir);
            if res.is_dir() {
                let _ = fs::remove_dir_all(&res);
            }
        }

        Ok(())
    } else {
        Err("路径不存在".to_string())
    }
}

#[tauri::command]
fn rename_entry(path: String, new_name: String) -> Result<String, String> {
    let p = PathBuf::from(&path);
    let parent = p
        .parent()
        .ok_or_else(|| "无法获取父目录".to_string())?;
    let new_path = parent.join(&new_name);

    // If this is a markdown file, rename its companion resources directory first.
    if let Some(old_res_dir) = resources_dir_path(&path) {
        let old_res = PathBuf::from(&old_res_dir);
        if old_res.is_dir() {
            let new_res_dir = resources_dir_path(&new_path.to_string_lossy());
            if let Some(new_res) = new_res_dir {
                let new_res_path = PathBuf::from(&new_res);
                if new_res_path != old_res {
                    let _ = fs::rename(&old_res, &new_res_path);
                }
            }
        }
    }

    fs::rename(&p, &new_path).map_err(|e| format!("无法重命名: {}", e))?;
    Ok(new_path.to_string_lossy().to_string())
}

#[tauri::command]
fn search_files(
    workspace: String,
    keyword: String,
) -> Result<Vec<FileSearchResult>, String> {
    if keyword.is_empty() {
        return Ok(Vec::new());
    }

    let pattern = regex::escape(&keyword);
    let mut builder = regex::RegexBuilder::new(&pattern);
    builder.case_insensitive(true);
    let re = builder
        .build()
        .map_err(|e| format!("正则表达式无效: {}", e))?;

    let mut results: Vec<FileSearchResult> = Vec::new();
    let workspace_path = std::path::Path::new(&workspace);

    walk_dir(
        workspace_path,
        workspace_path,
        &re,
        &mut results,
    )?;

    results.sort_by(|a, b| b.match_count.cmp(&a.match_count));
    Ok(results)
}

fn walk_dir(
    base: &std::path::Path,
    dir: &std::path::Path,
    re: &Regex,
    results: &mut Vec<FileSearchResult>,
) -> Result<(), String> {
    let entries = fs::read_dir(dir).map_err(|e| format!("无法读取目录: {}", e))?;

    for entry in entries {
        let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?;
        if is_hidden_entry(&entry) {
            continue;
        }

        let path = entry.path();
        if path.is_dir() {
            walk_dir(base, &path, re, results)?;
        } else if path.is_file() {
            let ext = path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("")
                .to_lowercase();
            if ext != "md" {
                continue;
            }

            let content = match fs::read_to_string(&path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            let lines: Vec<&str> = content.lines().collect();
            let mut matches: Vec<SearchMatch> = Vec::new();

            for (i, line) in lines.iter().enumerate() {
                if !re.is_match(line) {
                    continue;
                }

                let column = re
                    .find(line)
                    .map(|m| line[..m.start()].chars().count() + 1)
                    .unwrap_or(1);

                let context_before: Vec<String> = (0..i)
                    .rev()
                    .take(2)
                    .map(|j| lines[j].to_string())
                    .collect::<Vec<_>>()
                    .into_iter()
                    .rev()
                    .collect();

                let context_after: Vec<String> = lines
                    .iter()
                    .skip(i + 1)
                    .take(2)
                    .map(|s| s.to_string())
                    .collect();

                matches.push(SearchMatch {
                    line: i + 1,
                    column,
                    line_text: line.to_string(),
                    context_before,
                    context_after,
                });

                if matches.len() >= 100 {
                    break;
                }
            }

            if !matches.is_empty() {
                let relative_path = path
                    .strip_prefix(base)
                    .unwrap_or(&path)
                    .to_string_lossy()
                    .to_string();
                let match_count = matches.len();

                results.push(FileSearchResult {
                    relative_path,
                    absolute_path: path.to_string_lossy().to_string(),
                    matches,
                    match_count,
                });
            }
        }
    }

    Ok(())
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct SyncConfig {
    provider: SyncProvider,
    webdav: WebdavSyncConfig,
    s3: S3SyncConfig,
    auto_sync_on_startup: bool,
    auto_sync_after_save: bool,
    last_sync_at: Option<String>,
}

impl Default for SyncConfig {
    fn default() -> Self {
        Self {
            provider: SyncProvider::None,
            webdav: WebdavSyncConfig::default(),
            s3: S3SyncConfig::default(),
            auto_sync_on_startup: false,
            auto_sync_after_save: false,
            last_sync_at: None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
enum SyncProvider {
    None,
    Webdav,
    S3,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WebdavSyncConfig {
    endpoint: String,
    username: String,
    password: String,
    remote_dir: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct S3SyncConfig {
    endpoint: String,
    region: String,
    bucket: String,
    access_key_id: String,
    secret_access_key: String,
    remote_prefix: String,
    path_style: bool,
    use_https: bool,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct SyncHistory {
    provider_key: String,
    files: BTreeMap<String, SyncHistoryEntry>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct SyncHistoryEntry {
    local_modified_ms: Option<i128>,
    remote_modified_ms: Option<i128>,
    size: u64,
}

#[derive(Debug, Clone)]
struct SyncFileInfo {
    modified_ms: Option<i128>,
    size: u64,
}

#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncStats {
    uploaded: usize,
    downloaded: usize,
    deleted_local: usize,
    deleted_remote: usize,
    skipped: usize,
}

#[derive(Debug, Clone, Copy, serde::Serialize)]
#[serde(rename_all = "camelCase")]
enum SyncFileAction {
    Upload,
    Download,
    DeleteLocal,
    DeleteRemote,
    Skip,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncFileDetail {
    path: String,
    action: SyncFileAction,
    size: u64,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncResult {
    message: String,
    last_sync_at: String,
    stats: SyncStats,
    files: Vec<SyncFileDetail>,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncConfigSaveResult {
    config_path: String,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct SyncConnectionTestResult {
    message: String,
    details: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SyncAction {
    Upload,
    Download,
    DeleteLocal,
    DeleteRemote,
    Skip,
}

fn tauri_app_config_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
    app.path()
        .app_config_dir()
        .map_err(|e| format!("无法获取配置目录: {}", e))
}

fn sync_config_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
    Ok(tauri_app_config_dir(app)?.join("sync-config.json"))
}

fn sync_history_path(app: &tauri::AppHandle, provider_key: &str) -> Result<PathBuf, String> {
    Ok(tauri_app_config_dir(app)?.join(format!("sync-history-{}.json", stable_hash(provider_key))))
}

fn stable_hash(value: &str) -> u64 {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    value.hash(&mut hasher);
    hasher.finish()
}

fn load_sync_config_from_disk(app: &tauri::AppHandle) -> Result<SyncConfig, String> {
    let path = sync_config_path(app)?;
    if !path.exists() {
        return Ok(SyncConfig::default());
    }

    let raw = fs::read_to_string(&path).map_err(|e| format!("无法读取同步配置: {}", e))?;
    serde_json::from_str(&raw).map_err(|e| format!("同步配置格式无效: {}", e))
}

fn save_sync_config_to_disk(app: &tauri::AppHandle, config: &SyncConfig) -> Result<(), String> {
    let path = sync_config_path(app)?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("无法创建配置目录: {}", e))?;
    }

    let raw = serde_json::to_string_pretty(config).map_err(|e| format!("无法序列化同步配置: {}", e))?;
    fs::write(&path, raw).map_err(|e| format!("无法保存同步配置: {}", e))
}

fn load_sync_history(app: &tauri::AppHandle, provider_key: &str) -> Result<SyncHistory, String> {
    let path = sync_history_path(app, provider_key)?;
    if !path.exists() {
        return Ok(SyncHistory {
            provider_key: provider_key.to_string(),
            files: BTreeMap::new(),
        });
    }

    let raw = fs::read_to_string(&path).map_err(|e| format!("无法读取同步历史: {}", e))?;
    let mut history: SyncHistory =
        serde_json::from_str(&raw).map_err(|e| format!("同步历史格式无效: {}", e))?;
    history.provider_key = provider_key.to_string();
    Ok(history)
}

fn save_sync_history(app: &tauri::AppHandle, history: &SyncHistory) -> Result<(), String> {
    let path = sync_history_path(app, &history.provider_key)?;
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|e| format!("无法创建同步历史目录: {}", e))?;
    }

    let raw = serde_json::to_string_pretty(history).map_err(|e| format!("无法序列化同步历史: {}", e))?;
    fs::write(&path, raw).map_err(|e| format!("无法保存同步历史: {}", e))
}

fn normalize_remote_root(path: &str) -> String {
    let trimmed = path.trim().replace('\\', "/");
    let trimmed = trimmed.trim_matches('/');
    if trimmed.is_empty() {
        "/".to_string()
    } else {
        format!("/{}", trimmed)
    }
}

fn normalize_s3_prefix(prefix: &str) -> String {
    prefix
        .trim()
        .replace('\\', "/")
        .trim_matches('/')
        .to_string()
}

fn normalize_remote_file_path(path: &str) -> String {
    path.trim_start_matches('/').replace('\\', "/")
}

fn normalize_relative_path(path: &Path) -> String {
    path.components()
        .filter_map(|component| match component {
            std::path::Component::Normal(part) => Some(part.to_string_lossy().to_string()),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("/")
}

fn provider_key(config: &SyncConfig) -> Result<String, String> {
    match config.provider {
        SyncProvider::None => Err("请先选择同步方式".to_string()),
        SyncProvider::Webdav => {
            let endpoint = config.webdav.endpoint.trim();
            if endpoint.is_empty() {
                return Err("请填写 WebDAV 服务器地址".to_string());
            }
            Ok(format!(
                "webdav:{}:{}",
                endpoint.trim_end_matches('/'),
                normalize_remote_root(&config.webdav.remote_dir)
            ))
        }
        SyncProvider::S3 => {
            let bucket = config.s3.bucket.trim();
            if bucket.is_empty() {
                return Err("请填写 S3 Bucket".to_string());
            }
            Ok(format!(
                "s3:{}:{}:{}",
                config.s3.endpoint.trim().trim_end_matches('/'),
                bucket,
                normalize_s3_prefix(&config.s3.remote_prefix)
            ))
        }
    }
}

fn build_sync_operator(config: &SyncConfig) -> Result<Operator, String> {
    match config.provider {
        SyncProvider::None => Err("请先选择同步方式".to_string()),
        SyncProvider::Webdav => {
            let endpoint = config.webdav.endpoint.trim();
            if endpoint.is_empty() {
                return Err("请填写 WebDAV 服务器地址".to_string());
            }

            let mut builder = Webdav::default()
                .endpoint(endpoint)
                .root(&normalize_remote_root(&config.webdav.remote_dir));

            if !config.webdav.username.trim().is_empty() {
                builder = builder.username(config.webdav.username.trim());
            }
            if !config.webdav.password.is_empty() {
                builder = builder.password(&config.webdav.password);
            }

            Operator::new(builder)
                .map(|builder| builder.finish())
                .map_err(|e| format!("无法初始化 WebDAV 同步: {}", e))
        }
        SyncProvider::S3 => {
            let bucket = config.s3.bucket.trim();
            if bucket.is_empty() {
                return Err("请填写 S3 Bucket".to_string());
            }

            let mut endpoint = config.s3.endpoint.trim().trim_end_matches('/').to_string();
            if !endpoint.is_empty() && !endpoint.contains("://") {
                endpoint = if config.s3.use_https {
                    format!("https://{}", endpoint)
                } else {
                    format!("http://{}", endpoint)
                };
            }

            let mut builder = S3::default()
                .bucket(bucket)
                .root(&normalize_s3_prefix(&config.s3.remote_prefix))
                .disable_config_load();

            if !endpoint.is_empty() {
                builder = builder.endpoint(&endpoint);
            }
            if !config.s3.region.trim().is_empty() {
                builder = builder.region(config.s3.region.trim());
            }
            if !config.s3.access_key_id.trim().is_empty() {
                builder = builder.access_key_id(config.s3.access_key_id.trim());
            }
            if !config.s3.secret_access_key.is_empty() {
                builder = builder.secret_access_key(&config.s3.secret_access_key);
            }
            if !config.s3.path_style {
                builder = builder.enable_virtual_host_style();
            }

            Operator::new(builder)
                .map(|builder| builder.finish())
                .map_err(|e| format!("无法初始化 S3 同步: {}", e))
        }
    }
}

async fn opendal_timeout<T, F>(step: &str, timeout: Duration, future: F) -> Result<T, String>
where
    F: Future<Output = Result<T, opendal::Error>>,
{
    match tokio::time::timeout(timeout, future).await {
        Ok(Ok(value)) => Ok(value),
        Ok(Err(error)) => Err(format!("{}失败: {}", step, describe_opendal_error(&error))),
        Err(_) => Err(format!("{}超时（{} 秒）", step, timeout.as_secs())),
    }
}

async fn test_step<T, F>(
    details: &mut Vec<String>,
    step: &str,
    success: &str,
    future: F,
) -> Result<T, String>
where
    F: Future<Output = Result<T, opendal::Error>>,
{
    let start = Instant::now();
    match tokio::time::timeout(SYNC_TEST_STEP_TIMEOUT, future).await {
        Ok(Ok(value)) => {
            details.push(format!(
                "{}: {}，耗时 {}ms",
                step,
                success,
                start.elapsed().as_millis()
            ));
            Ok(value)
        }
        Ok(Err(error)) => Err(format_sync_test_failure(
            step,
            describe_opendal_error(&error),
            details,
        )),
        Err(_) => Err(format_sync_test_failure(
            step,
            format!("请求超时（{} 秒），请检查服务器地址、网络或远端目录响应速度", SYNC_TEST_STEP_TIMEOUT.as_secs()),
            details,
        )),
    }
}

fn describe_opendal_error(error: &opendal::Error) -> String {
    let hint = match error.kind() {
        ErrorKind::ConfigInvalid => "配置无效，请检查服务器地址、Bucket、Region 或远端路径",
        ErrorKind::PermissionDenied => "权限被拒绝，通常表示账号/密码/Access Key 不正确，或该账号没有远端目录权限",
        ErrorKind::NotFound => "远端路径不存在，请检查 WebDAV 远端目录、S3 Bucket 或前缀",
        ErrorKind::Unsupported => "服务不支持当前操作，请确认 WebDAV/S3 兼容能力",
        ErrorKind::RateLimited => "服务限流，请稍后重试",
        ErrorKind::IsADirectory => "目标是目录，不是文件",
        ErrorKind::NotADirectory => "目标不是目录",
        ErrorKind::AlreadyExists => "目标已存在",
        ErrorKind::ConditionNotMatch => "服务端条件校验未通过",
        ErrorKind::RangeNotSatisfied => "读取范围无效",
        ErrorKind::IsSameFile => "源和目标是同一个文件",
        ErrorKind::Unexpected => "服务返回了非预期错误，请检查原始错误信息",
        _ => "服务返回了未知错误",
    };

    format!("{}；原始错误: {}", hint, error)
}

fn format_sync_test_failure(step: &str, reason: String, details: &[String]) -> String {
    let mut lines = vec![format!("连接测试失败，失败步骤：{}", step), reason];
    if !details.is_empty() {
        lines.push("已通过步骤：".to_string());
        lines.extend(details.iter().map(|detail| format!("- {}", detail)));
    }
    lines.join("\n")
}

fn sync_target_label(config: &SyncConfig) -> String {
    match config.provider {
        SyncProvider::None => "未选择同步方式".to_string(),
        SyncProvider::Webdav => format!(
            "WebDAV {}{}",
            config.webdav.endpoint.trim(),
            normalize_remote_root(&config.webdav.remote_dir)
        ),
        SyncProvider::S3 => format!(
            "S3 bucket={} prefix={}",
            config.s3.bucket.trim(),
            normalize_s3_prefix(&config.s3.remote_prefix)
        ),
    }
}

fn sync_probe_file_path() -> String {
    let millis = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or_default();
    format!("_yurou-sync-probe-{}-{}.tmp", std::process::id(), millis)
}

fn remote_parent_dirs(relative: &str) -> Vec<String> {
    let parts: Vec<&str> = relative.split('/').filter(|part| !part.is_empty()).collect();
    if parts.len() <= 1 {
        return Vec::new();
    }

    let mut dirs = Vec::new();
    let mut current = String::new();
    for part in parts.iter().take(parts.len() - 1) {
        current.push_str(part);
        current.push('/');
        dirs.push(current.clone());
    }
    dirs
}

async fn ensure_remote_parent_dirs(op: &Operator, relative: &str) -> Result<(), String> {
    for dir in remote_parent_dirs(relative) {
        match tokio::time::timeout(SYNC_REMOTE_FILE_TIMEOUT, op.create_dir(&dir)).await {
            Ok(Ok(())) => {}
            Ok(Err(error)) if error.kind() == ErrorKind::AlreadyExists => {}
            Ok(Err(error)) => {
                return Err(format!(
                    "创建远端目录 {}失败: {}",
                    dir,
                    describe_opendal_error(&error)
                ));
            }
            Err(_) => {
                return Err(format!(
                    "创建远端目录 {}超时（{} 秒）",
                    dir,
                    SYNC_REMOTE_FILE_TIMEOUT.as_secs()
                ));
            }
        }
    }
    Ok(())
}

async fn run_sync_connection_test(config: &SyncConfig) -> Result<SyncConnectionTestResult, String> {
    let mut details = Vec::new();
    let op = build_sync_operator(config)?;
    details.push(format!("配置解析通过，目标：{}", sync_target_label(config)));

    test_step(&mut details, "服务检查", "服务器可达", op.check()).await?;
    test_step(
        &mut details,
        "目录读取",
        "账号/密码有效，远端目录可读取",
        op.list_with("/").limit(1).into_future(),
    )
    .await?;

    let probe_path = sync_probe_file_path();
    test_step(
        &mut details,
        "写入探针",
        "远端目录可写",
        op.write(&probe_path, SYNC_TEST_PAYLOAD.to_vec()),
    )
    .await?;

    let read_result = test_step(
        &mut details,
        "读取探针",
        "写入内容可读回",
        op.read(&probe_path),
    )
    .await;
    let read_bytes = match read_result {
        Ok(bytes) => bytes.to_vec(),
        Err(error) => {
            let _ = opendal_timeout("清理测试文件", SYNC_TEST_STEP_TIMEOUT, op.delete(&probe_path)).await;
            return Err(error);
        }
    };

    if read_bytes != SYNC_TEST_PAYLOAD {
        let _ = opendal_timeout("清理测试文件", SYNC_TEST_STEP_TIMEOUT, op.delete(&probe_path)).await;
        return Err(format_sync_test_failure(
            "读取探针",
            "读回内容与写入内容不一致，请检查 WebDAV/S3 服务是否有缓存或代理改写".to_string(),
            &details,
        ));
    }

    test_step(
        &mut details,
        "删除探针",
        "远端目录可删除",
        op.delete(&probe_path),
    )
    .await?;

    Ok(SyncConnectionTestResult {
        message: "连接可用，账号/密码正确，远端目录具备读取、写入和删除权限".to_string(),
        details,
    })
}

fn system_time_ms(time: SystemTime) -> Option<i128> {
    match time.duration_since(UNIX_EPOCH) {
        Ok(duration) => Some(duration.as_millis() as i128),
        Err(error) => Some(-(error.duration().as_millis() as i128)),
    }
}

fn now_string() -> String {
    opendal::raw::Timestamp::now().to_string()
}

fn remote_modified_ms(meta: &opendal::Metadata) -> Option<i128> {
    meta.last_modified()
        .and_then(|value| {
            let system_time: SystemTime = value.into();
            system_time_ms(system_time)
        })
}

fn scan_local_files(root: &Path) -> Result<BTreeMap<String, SyncFileInfo>, String> {
    if !root.is_dir() {
        return Err("工作目录不存在或不是文件夹".to_string());
    }

    let mut files = BTreeMap::new();
    scan_local_files_inner(root, root, &mut files)?;
    Ok(files)
}

fn scan_local_files_inner(
    root: &Path,
    current: &Path,
    files: &mut BTreeMap<String, SyncFileInfo>,
) -> Result<(), String> {
    for entry in fs::read_dir(current).map_err(|e| format!("无法读取工作目录: {}", e))? {
        let entry = entry.map_err(|e| format!("读取工作目录条目失败: {}", e))?;
        let path = entry.path();
        let file_type = entry
            .file_type()
            .map_err(|e| format!("无法读取文件类型: {}", e))?;

        if file_type.is_dir() {
            scan_local_files_inner(root, &path, files)?;
        } else if file_type.is_file() {
            let metadata = entry
                .metadata()
                .map_err(|e| format!("无法读取文件元数据: {}", e))?;
            let relative = path
                .strip_prefix(root)
                .map(normalize_relative_path)
                .map_err(|e| format!("无法计算相对路径: {}", e))?;
            files.insert(
                relative.clone(),
                SyncFileInfo {
                    modified_ms: metadata.modified().ok().and_then(system_time_ms),
                    size: metadata.len(),
                },
            );
        }
    }

    Ok(())
}

async fn scan_remote_files(op: &Operator) -> Result<BTreeMap<String, SyncFileInfo>, String> {
    let entries = opendal_timeout(
        "读取远端文件列表",
        SYNC_REMOTE_LIST_TIMEOUT,
        op.list_with("/").recursive(true).into_future(),
    )
    .await?;

    let mut files = BTreeMap::new();
    for entry in entries {
        let (path, listed_meta) = entry.into_parts();
        if path == "/" || path.ends_with('/') || listed_meta.mode() == EntryMode::DIR {
            continue;
        }

        let meta = match tokio::time::timeout(SYNC_REMOTE_FILE_TIMEOUT, op.stat(&path)).await {
            Ok(Ok(meta)) => meta,
            Ok(Err(error)) if error.kind() == ErrorKind::NotFound => continue,
            Ok(Err(error)) => {
                return Err(format!(
                    "读取远端文件元数据 {}失败: {}",
                    path,
                    describe_opendal_error(&error)
                ));
            }
            Err(_) => {
                return Err(format!(
                    "读取远端文件元数据 {}超时（{} 秒）",
                    path,
                    SYNC_REMOTE_FILE_TIMEOUT.as_secs()
                ));
            }
        };

        if !meta.is_file() {
            continue;
        }

        let relative = normalize_remote_file_path(&path);
        files.insert(
            relative.clone(),
            SyncFileInfo {
                modified_ms: remote_modified_ms(&meta),
                size: meta.content_length(),
            },
        );
    }

    Ok(files)
}

fn choose_sync_action(
    local: Option<&SyncFileInfo>,
    remote: Option<&SyncFileInfo>,
    history: Option<&SyncHistoryEntry>,
) -> SyncAction {
    match (local, remote) {
        (Some(local), Some(remote)) => {
            if let Some(history) = history {
                if history.local_modified_ms == local.modified_ms
                    && history.remote_modified_ms == remote.modified_ms
                    && history.size == local.size
                    && history.size == remote.size
                {
                    return SyncAction::Skip;
                }
            }

            if local.size == remote.size
                && local.modified_ms.is_some()
                && remote.modified_ms.is_some()
                && local.modified_ms == remote.modified_ms
            {
                return SyncAction::Skip;
            }

            match (local.modified_ms, remote.modified_ms) {
                (Some(local_ms), Some(remote_ms)) => {
                    if local_ms >= remote_ms {
                        SyncAction::Upload
                    } else {
                        SyncAction::Download
                    }
                }
                (Some(_), None) => SyncAction::Upload,
                (None, Some(_)) => SyncAction::Download,
                (None, None) => {
                    if local.size == remote.size {
                        SyncAction::Skip
                    } else {
                        SyncAction::Upload
                    }
                }
            }
        }
        (Some(local), None) => {
            if let Some(history) = history {
                if history.local_modified_ms == local.modified_ms && history.size == local.size {
                    SyncAction::DeleteLocal
                } else {
                    SyncAction::Upload
                }
            } else {
                SyncAction::Upload
            }
        }
        (None, Some(remote)) => {
            if let Some(history) = history {
                if history.remote_modified_ms == remote.modified_ms && history.size == remote.size {
                    SyncAction::DeleteRemote
                } else {
                    SyncAction::Download
                }
            } else {
                SyncAction::Download
            }
        }
        (None, None) => SyncAction::Skip,
    }
}

fn local_path_for(root: &Path, relative: &str) -> PathBuf {
    relative
        .split('/')
        .filter(|part| !part.is_empty())
        .fold(root.to_path_buf(), |path, part| path.join(part))
}

async fn sync_workspace(
    app: &tauri::AppHandle,
    workspace: &Path,
    config: &SyncConfig,
) -> Result<SyncResult, String> {
    let op = build_sync_operator(config)?;
    opendal_timeout("同步连接检查", SYNC_CHECK_TIMEOUT, op.check()).await?;

    let key = provider_key(config)?;
    let local_files = scan_local_files(workspace)?;
    let remote_files = scan_remote_files(&op).await?;
    let mut history = load_sync_history(app, &key)?;
    let mut stats = SyncStats::default();
    let mut file_details = Vec::new();

    let all_paths: BTreeSet<String> = local_files
        .keys()
        .chain(remote_files.keys())
        .chain(history.files.keys())
        .cloned()
        .collect();

    for relative in all_paths {
        let local = local_files.get(&relative);
        let remote = remote_files.get(&relative);
        let prior = history.files.get(&relative);

        match choose_sync_action(local, remote, prior) {
            SyncAction::Upload => {
                let local_path = local_path_for(workspace, &relative);
                let bytes = fs::read(&local_path)
                    .map_err(|e| format!("无法读取本地文件 {}: {}", local_path.display(), e))?;
                ensure_remote_parent_dirs(&op, &relative).await?;
                opendal_timeout(
                    &format!("上传 {}", relative),
                    SYNC_REMOTE_FILE_TIMEOUT,
                    op.write(&relative, bytes),
                )
                .await?;
                stats.uploaded += 1;
                file_details.push(SyncFileDetail {
                    path: relative.clone(),
                    action: SyncFileAction::Upload,
                    size: local.map(|item| item.size).unwrap_or(0),
                });
            }
            SyncAction::Download => {
                let local_path = local_path_for(workspace, &relative);
                if let Some(parent) = local_path.parent() {
                    fs::create_dir_all(parent)
                        .map_err(|e| format!("无法创建本地目录 {}: {}", parent.display(), e))?;
                }
                let bytes = opendal_timeout(
                    &format!("下载 {}", relative),
                    SYNC_REMOTE_FILE_TIMEOUT,
                    op.read(&relative),
                )
                .await?
                .to_vec();
                fs::write(&local_path, bytes)
                    .map_err(|e| format!("无法写入本地文件 {}: {}", local_path.display(), e))?;
                stats.downloaded += 1;
                file_details.push(SyncFileDetail {
                    path: relative.clone(),
                    action: SyncFileAction::Download,
                    size: remote.map(|item| item.size).unwrap_or(0),
                });
            }
            SyncAction::DeleteLocal => {
                let local_path = local_path_for(workspace, &relative);
                if local_path.is_file() {
                    fs::remove_file(&local_path)
                        .map_err(|e| format!("无法删除本地文件 {}: {}", local_path.display(), e))?;
                }
                history.files.remove(&relative);
                stats.deleted_local += 1;
                file_details.push(SyncFileDetail {
                    path: relative.clone(),
                    action: SyncFileAction::DeleteLocal,
                    size: local.map(|item| item.size).unwrap_or(0),
                });
                continue;
            }
            SyncAction::DeleteRemote => {
                opendal_timeout(
                    &format!("删除远端文件 {}", relative),
                    SYNC_REMOTE_FILE_TIMEOUT,
                    op.delete(&relative),
                )
                .await?;
                history.files.remove(&relative);
                stats.deleted_remote += 1;
                file_details.push(SyncFileDetail {
                    path: relative.clone(),
                    action: SyncFileAction::DeleteRemote,
                    size: remote.map(|item| item.size).unwrap_or(0),
                });
                continue;
            }
            SyncAction::Skip => {
                stats.skipped += 1;
                file_details.push(SyncFileDetail {
                    path: relative.clone(),
                    action: SyncFileAction::Skip,
                    size: local
                        .map(|item| item.size)
                        .or_else(|| remote.map(|item| item.size))
                        .unwrap_or(0),
                });
            }
        }
    }

    let refreshed_local = scan_local_files(workspace)?;
    let refreshed_remote = scan_remote_files(&op).await?;
    history.files.clear();

    let synced_paths: BTreeSet<String> = refreshed_local
        .keys()
        .chain(refreshed_remote.keys())
        .cloned()
        .collect();

    for relative in synced_paths {
        let local = refreshed_local.get(&relative);
        let remote = refreshed_remote.get(&relative);
        let size = local
            .map(|item| item.size)
            .or_else(|| remote.map(|item| item.size))
            .unwrap_or(0);
        history.files.insert(
            relative,
            SyncHistoryEntry {
                local_modified_ms: local.and_then(|item| item.modified_ms),
                remote_modified_ms: remote.and_then(|item| item.modified_ms),
                size,
            },
        );
    }

    save_sync_history(app, &history)?;

    let last_sync_at = now_string();
    let mut saved_config = config.clone();
    saved_config.last_sync_at = Some(last_sync_at.clone());
    save_sync_config_to_disk(app, &saved_config)?;

    Ok(SyncResult {
        message: "同步完成".to_string(),
        last_sync_at,
        stats,
        files: file_details,
    })
}

#[tauri::command]
fn load_sync_config(app: tauri::AppHandle) -> Result<SyncConfig, String> {
    load_sync_config_from_disk(&app)
}

#[tauri::command]
fn save_sync_config(
    app: tauri::AppHandle,
    config: SyncConfig,
) -> Result<SyncConfigSaveResult, String> {
    save_sync_config_to_disk(&app, &config)?;
    let config_path = sync_config_path(&app)?.to_string_lossy().into_owned();
    Ok(SyncConfigSaveResult { config_path })
}

#[tauri::command]
async fn test_sync_connection(config: SyncConfig) -> Result<SyncConnectionTestResult, String> {
    run_sync_connection_test(&config).await
}

#[tauri::command]
async fn sync_now(app: tauri::AppHandle, workspace: String, config: SyncConfig) -> Result<SyncResult, String> {
    let workspace = PathBuf::from(workspace);
    sync_workspace(&app, &workspace, &config).await
}

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_dialog::init())
        .manage(WorkspaceWatcherState::default())
        .invoke_handler(tauri::generate_handler![
            greet,
            pick_folder,
            read_dir,
            read_file,
            write_file,
            watch_workspace,
            unwatch_workspace,
            pick_save_file,
            create_folder,
            create_file,
            delete_entry,
            rename_entry,
            save_media_file,
            search_files,
            load_sync_config,
            save_sync_config,
            test_sync_connection,
            sync_now
        ])
        .setup(|app| {
            // ---- File menu ----
            let new = MenuItemBuilder::with_id("new", "新建")
                .accelerator("CmdOrCtrl+N")
                .build(app)?;
            let open = MenuItemBuilder::with_id("open", "打开文件夹...")
                .accelerator("CmdOrCtrl+O")
                .build(app)?;
            let save = MenuItemBuilder::with_id("save", "保存")
                .accelerator("CmdOrCtrl+S")
                .build(app)?;
            let save_as = MenuItemBuilder::with_id("save_as", "另存为...")
                .accelerator("CmdOrCtrl+Shift+S")
                .build(app)?;
            let quit = MenuItemBuilder::with_id("quit", "退出")
                .accelerator("CmdOrCtrl+Q")
                .build(app)?;

            let file_menu = SubmenuBuilder::new(app, "文件")
                .item(&new)
                .item(&open)
                .item(&save)
                .item(&save_as)
                .separator()
                .item(&quit)
                .build()?;

            // ---- Edit menu ----
            let undo = MenuItemBuilder::with_id("undo", "撤销")
                .accelerator("CmdOrCtrl+Z")
                .build(app)?;
            let redo = MenuItemBuilder::with_id("redo", "重做")
                .accelerator("CmdOrCtrl+Shift+Z")
                .build(app)?;
            let cut = MenuItemBuilder::with_id("cut", "剪切")
                .accelerator("CmdOrCtrl+X")
                .build(app)?;
            let copy = MenuItemBuilder::with_id("copy", "复制")
                .accelerator("CmdOrCtrl+C")
                .build(app)?;
            let paste = MenuItemBuilder::with_id("paste", "粘贴")
                .accelerator("CmdOrCtrl+V")
                .build(app)?;
            let select_all = MenuItemBuilder::with_id("select_all", "全选")
                .accelerator("CmdOrCtrl+A")
                .build(app)?;

            let edit_menu = SubmenuBuilder::new(app, "编辑")
                .item(&undo)
                .item(&redo)
                .separator()
                .item(&cut)
                .item(&copy)
                .item(&paste)
                .separator()
                .item(&select_all)
                .build()?;

            // ---- View menu ----
            let toggle_outline = MenuItemBuilder::with_id("toggle_outline", "切换大纲")
                .accelerator("CmdOrCtrl+B")
                .build(app)?;
            let search = MenuItemBuilder::with_id("search", "搜索")
                .accelerator("CmdOrCtrl+Shift+F")
                .build(app)?;

            let view_menu = SubmenuBuilder::new(app, "视图")
                .item(&toggle_outline)
                .separator()
                .item(&search)
                .build()?;

            // ---- Help menu ----
            let about = MenuItemBuilder::with_id("about", "关于").build(app)?;

            let help_menu = SubmenuBuilder::new(app, "帮助").item(&about).build()?;

            // ---- Build the menu bar ----
            let menu = MenuBuilder::new(app)
                .item(&file_menu)
                .item(&edit_menu)
                .item(&view_menu)
                .item(&help_menu)
                .build()?;

            app.set_menu(menu)?;

            // ---- Handle menu events ----
            let app_handle = app.handle().clone();
            app.on_menu_event(move |_app_handle, event| {
                let id = event.id().0.as_str();
                match id {
                    "quit" => {
                        let _ = app_handle.emit("menu-quit", ());
                    }
                    "open" => {
                        let handle = app_handle.clone();
                        // Spawn because dialog is blocking
                        std::thread::spawn(move || {
                            if let Some(folder) = handle.dialog().file().blocking_pick_folder() {
                                let path = folder.to_string();
                                let _ = handle.emit("folder-opened", path);
                            }
                        });
                    }
                    "save" => {
                        let _ = app_handle.emit("menu-save", ());
                    }
                    "save_as" => {
                        let _ = app_handle.emit("menu-save-as", ());
                    }
                    "new" => {
                        let _ = app_handle.emit("menu-new", ());
                    }
                    "toggle_outline" => {
                        let _ = app_handle.emit("toggle-outline", ());
                    }
                    "about" => {
                        let _ = app_handle.emit("show-about", ());
                    }
                    "search" => {
                        let _ = app_handle.emit("menu-search", ());
                    }
                    _ => {
                        // Forward other menu events to frontend
                        let _ = app_handle.emit("menu-event", id);
                    }
                }
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
