use reqwest::Method;
use reqwest::StatusCode;
use rusqlite::{params, Connection};
use serde::Serialize;
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::io::Write;
use std::io::{Read, Seek, SeekFrom};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(target_os = "windows")]
use std::process::Stdio;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::Emitter;
use tokio::time::sleep;

#[cfg(target_os = "windows")]
use windows_sys::Win32::Security::Cryptography::{
    CryptProtectData, CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB,
};
#[cfg(target_os = "windows")]
use windows_sys::Win32::Foundation::LocalFree;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[cfg(target_os = "windows")]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
#[cfg(target_os = "windows")]
const DETACHED_PROCESS: u32 = 0x00000008;
const RESUMABLE_CHUNK_MAX_RETRIES: u32 = 3;
const RESUMABLE_CHUNK_RETRY_BASE_DELAY_MS: u64 = 900;

struct ApiState {
    client: reqwest::Client,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BridgeResponse {
    ok: bool,
    status: u16,
    data: Value,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct NativeDownloadProgressPayload {
    task_id: String,
    downloaded_bytes: u64,
    total_bytes: Option<u64>,
    progress: Option<f64>,
    resumed_bytes: u64,
    done: bool,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct NativeUploadProgressPayload {
    task_id: String,
    uploaded_bytes: u64,
    total_bytes: u64,
    progress: f64,
    done: bool,
}

fn emit_native_download_progress(
    window: &tauri::WebviewWindow,
    task_id: &str,
    downloaded_bytes: u64,
    total_bytes: Option<u64>,
    resumed_bytes: u64,
    done: bool,
) {
    if task_id.trim().is_empty() {
        return;
    }

    let progress = total_bytes
        .filter(|total| *total > 0)
        .map(|total| (downloaded_bytes as f64 / total as f64) * 100.0);

    let payload = NativeDownloadProgressPayload {
        task_id: task_id.to_string(),
        downloaded_bytes,
        total_bytes,
        progress,
        resumed_bytes,
        done,
    };

    if let Err(err) = window.emit("native-download-progress", payload) {
        eprintln!("emit native-download-progress failed: {}", err);
    }
}

fn emit_native_upload_progress(
    window: &tauri::WebviewWindow,
    task_id: &str,
    uploaded_bytes: u64,
    total_bytes: u64,
    done: bool,
) {
    if task_id.trim().is_empty() {
        return;
    }

    let normalized_total = total_bytes.max(1);
    let progress = (uploaded_bytes as f64 / normalized_total as f64) * 100.0;
    let payload = NativeUploadProgressPayload {
        task_id: task_id.to_string(),
        uploaded_bytes,
        total_bytes,
        progress,
        done,
    };

    if let Err(err) = window.emit("native-upload-progress", payload) {
        eprintln!("emit native-upload-progress failed: {}", err);
    }
}

fn normalize_base_url(base_url: &str) -> String {
    let trimmed = base_url.trim();
    if trimmed.is_empty() {
        return String::new();
    }
    trimmed.trim_end_matches('/').to_string()
}

fn join_api_url(base_url: &str, path: &str) -> String {
    format!("{}{}", normalize_base_url(base_url), path)
}

fn sanitize_device_id_component(raw: &str) -> String {
    let mut output = String::new();
    let mut last_is_dash = false;
    for ch in raw.chars() {
        let normalized = ch.to_ascii_lowercase();
        if normalized.is_ascii_alphanumeric() {
            output.push(normalized);
            last_is_dash = false;
        } else if !last_is_dash {
            output.push('-');
            last_is_dash = true;
        }
    }
    output.trim_matches('-').to_string()
}

fn build_desktop_client_meta() -> (String, String, String) {
    let os = match env::consts::OS {
        "windows" => "Windows",
        "macos" => "macOS",
        "linux" => "Linux",
        other => other,
    };
    let platform = format!("{}-{}", os, env::consts::ARCH);
    let host_name = env::var("COMPUTERNAME")
        .or_else(|_| env::var("HOSTNAME"))
        .unwrap_or_default();
    let host_trimmed = host_name.trim();
    let device_name = if host_trimmed.is_empty() {
        format!("桌面客户端 · {}", platform)
    } else {
        format!("{} · {}", host_trimmed, platform)
    };
    let id_seed = if host_trimmed.is_empty() {
        platform.clone()
    } else {
        format!("{}-{}", host_trimmed, platform)
    };
    let normalized = sanitize_device_id_component(&id_seed);
    let device_id = if normalized.is_empty() {
        "desktop-client".to_string()
    } else {
        format!("desktop-{}", normalized)
    };
    (platform, device_name, device_id)
}

fn build_upload_file_fingerprint(meta: &fs::Metadata) -> Option<String> {
    let size = meta.len();
    let modified_ms = meta
        .modified()
        .ok()
        .and_then(|ts| ts.duration_since(UNIX_EPOCH).ok())
        .map(|duration| duration.as_millis())
        .unwrap_or(0);
    let fingerprint = format!("v1:size:{}:mtime:{}", size, modified_ms);
    if fingerprint.len() > 120 {
        None
    } else {
        Some(fingerprint)
    }
}

fn is_retryable_upload_status(status: u16) -> bool {
    matches!(status, 408 | 425 | 429 | 500 | 502 | 503 | 504)
}

fn is_retryable_transport_error(err: &reqwest::Error) -> bool {
    err.is_timeout() || err.is_connect() || err.is_request() || err.is_body()
}

fn build_chunk_retry_delay(attempt: u32) -> Duration {
    let multiplier = 2_u64.saturating_pow(attempt.min(5));
    let ms = RESUMABLE_CHUNK_RETRY_BASE_DELAY_MS.saturating_mul(multiplier).min(15_000);
    Duration::from_millis(ms)
}

fn to_hex_string(bytes: &[u8]) -> String {
    let mut output = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        output.push_str(&format!("{:02x}", b));
    }
    output
}

fn compute_file_sha256_hex(file_path: &Path) -> Result<String, String> {
    let mut file = fs::File::open(file_path).map_err(|err| format!("打开文件失败: {}", err))?;
    let mut hasher = Sha256::new();
    let mut buf = vec![0_u8; 1024 * 256];
    loop {
        let read = file
            .read(&mut buf)
            .map_err(|err| format!("读取文件失败: {}", err))?;
        if read == 0 {
            break;
        }
        hasher.update(&buf[..read]);
    }
    Ok(to_hex_string(&hasher.finalize()))
}

#[cfg(target_os = "windows")]
fn dpapi_protect_bytes(input: &[u8]) -> Result<Vec<u8>, String> {
    if input.is_empty() {
        return Ok(Vec::new());
    }

    let mut in_blob = CRYPT_INTEGER_BLOB {
        cbData: input.len() as u32,
        pbData: input.as_ptr() as *mut u8,
    };
    let mut out_blob = CRYPT_INTEGER_BLOB {
        cbData: 0,
        pbData: std::ptr::null_mut(),
    };

    let ok = unsafe {
        CryptProtectData(
            &mut in_blob,
            std::ptr::null(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            CRYPTPROTECT_UI_FORBIDDEN,
            &mut out_blob,
        )
    };
    if ok == 0 {
        return Err(format!("加密登录状态失败: {}", std::io::Error::last_os_error()));
    }

    let data = unsafe {
        std::slice::from_raw_parts(out_blob.pbData as *const u8, out_blob.cbData as usize).to_vec()
    };
    unsafe {
        LocalFree(out_blob.pbData as *mut core::ffi::c_void);
    }
    Ok(data)
}

#[cfg(target_os = "windows")]
fn dpapi_unprotect_bytes(input: &[u8]) -> Result<Vec<u8>, String> {
    if input.is_empty() {
        return Ok(Vec::new());
    }

    let mut in_blob = CRYPT_INTEGER_BLOB {
        cbData: input.len() as u32,
        pbData: input.as_ptr() as *mut u8,
    };
    let mut out_blob = CRYPT_INTEGER_BLOB {
        cbData: 0,
        pbData: std::ptr::null_mut(),
    };

    let ok = unsafe {
        CryptUnprotectData(
            &mut in_blob,
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            std::ptr::null_mut(),
            CRYPTPROTECT_UI_FORBIDDEN,
            &mut out_blob,
        )
    };
    if ok == 0 {
        return Err(format!("解密登录状态失败: {}", std::io::Error::last_os_error()));
    }

    let data = unsafe {
        std::slice::from_raw_parts(out_blob.pbData as *const u8, out_blob.cbData as usize).to_vec()
    };
    unsafe {
        LocalFree(out_blob.pbData as *mut core::ffi::c_void);
    }
    Ok(data)
}

fn encode_login_password(raw_password: &str) -> Result<String, String> {
    #[cfg(target_os = "windows")]
    {
        let protected = dpapi_protect_bytes(raw_password.as_bytes())?;
        return Ok(format!("dpapi:{}", to_hex_string(&protected)));
    }

    #[cfg(not(target_os = "windows"))]
    {
        Ok(raw_password.to_string())
    }
}

fn decode_login_password(stored_password: &str) -> Result<String, String> {
    let raw = stored_password.trim();
    if let Some(hex_body) = raw.strip_prefix("dpapi:") {
        #[cfg(target_os = "windows")]
        {
            if hex_body.len() % 2 != 0 {
                return Err("登录状态密文格式无效".to_string());
            }
            let mut encrypted = Vec::with_capacity(hex_body.len() / 2);
            let bytes = hex_body.as_bytes();
            let mut index = 0;
            while index < bytes.len() {
                let part = std::str::from_utf8(&bytes[index..index + 2])
                    .map_err(|_| "登录状态密文格式无效".to_string())?;
                let value = u8::from_str_radix(part, 16)
                    .map_err(|_| "登录状态密文格式无效".to_string())?;
                encrypted.push(value);
                index += 2;
            }
            let plain = dpapi_unprotect_bytes(&encrypted)?;
            return String::from_utf8(plain).map_err(|_| "登录状态密文解码失败".to_string());
        }

        #[cfg(not(target_os = "windows"))]
        {
            let _ = hex_body;
            return Err("当前系统不支持读取该登录状态密文".to_string());
        }
    }
    Ok(raw.to_string())
}

fn fallback_json(status: StatusCode, text: &str) -> Value {
    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(status.is_success()));
    data.insert(
        "message".to_string(),
        Value::String(if text.trim().is_empty() {
            format!("HTTP {}", status.as_u16())
        } else {
            text.to_string()
        }),
    );
    Value::Object(data)
}

fn sanitize_file_name(name: &str) -> String {
    let raw = name.trim();
    let mut cleaned = String::with_capacity(raw.len());
    for ch in raw.chars() {
        if matches!(ch, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' | '\0') {
            cleaned.push('_');
        } else {
            cleaned.push(ch);
        }
    }
    let normalized = cleaned.trim().trim_matches('.').to_string();
    if normalized.is_empty() {
        "download.bin".to_string()
    } else {
        normalized
    }
}

fn resolve_download_dir() -> PathBuf {
    if let Some(home) = env::var_os("USERPROFILE") {
        return PathBuf::from(home).join("Downloads");
    }
    if let Some(home) = env::var_os("HOME") {
        return PathBuf::from(home).join("Downloads");
    }
    PathBuf::from(".")
}

fn split_file_name(name: &str) -> (String, String) {
    if let Some(index) = name.rfind('.') {
        if index > 0 && index < name.len() - 1 {
            let stem = name[..index].to_string();
            let ext = name[index + 1..].to_string();
            return (stem, ext);
        }
    }
    (name.to_string(), String::new())
}

fn alloc_download_path(download_dir: &Path, preferred_name: &str) -> PathBuf {
    let safe_name = sanitize_file_name(preferred_name);
    let first = download_dir.join(&safe_name);
    if !first.exists() {
        return first;
    }

    let (stem, ext) = split_file_name(&safe_name);
    for index in 1..10000 {
        let candidate_name = if ext.is_empty() {
            format!("{} ({})", stem, index)
        } else {
            format!("{} ({}).{}", stem, index, ext)
        };
        let candidate = download_dir.join(candidate_name);
        if !candidate.exists() {
            return candidate;
        }
    }

    first
}

fn build_download_resume_temp_path(download_dir: &Path, preferred_name: &str, url: &str) -> PathBuf {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    use std::hash::{Hash, Hasher};
    preferred_name.hash(&mut hasher);
    url.hash(&mut hasher);
    let digest = format!("{:016x}", hasher.finish());
    let safe_name = sanitize_file_name(preferred_name);
    let temp_name = format!(".{}.{}.part", safe_name, digest);
    download_dir.join(temp_name)
}

fn is_update_installer_file_name(file_name: &str) -> bool {
    let lower = file_name.trim().to_ascii_lowercase();
    if !lower.ends_with(".exe") {
        return false;
    }
    lower.starts_with("wanwan-cloud-desktop_v") || file_name.trim().starts_with("玩玩云_v")
}

fn cleanup_old_update_installers(
    download_dir: &Path,
    keep_file_name: &str,
    keep_latest: usize,
) -> Result<(), String> {
    let mut entries: Vec<(PathBuf, SystemTime)> = Vec::new();
    let normalized_keep = keep_file_name.trim();
    for entry in fs::read_dir(download_dir).map_err(|err| format!("扫描下载目录失败: {}", err))? {
        let path = match entry {
            Ok(item) => item.path(),
            Err(_) => continue,
        };
        if !path.is_file() {
            continue;
        }
        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
            continue;
        };
        if !is_update_installer_file_name(file_name) {
            continue;
        }
        let modified = fs::metadata(&path)
            .ok()
            .and_then(|meta| meta.modified().ok())
            .unwrap_or(SystemTime::UNIX_EPOCH);
        entries.push((path, modified));
    }

    entries.sort_by(|a, b| b.1.cmp(&a.1));
    let retain_count = keep_latest.max(1);
    let mut retained = 0usize;
    for (path, _) in entries {
        let file_name = path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or_default()
            .to_string();
        let should_keep = file_name == normalized_keep || retained < retain_count;
        if should_keep {
            retained += 1;
            continue;
        }
        let _ = fs::remove_file(&path);
    }
    Ok(())
}

fn resolve_local_state_dir() -> PathBuf {
    if let Some(appdata) = env::var_os("APPDATA") {
        return PathBuf::from(appdata).join("wanwan-cloud-desktop");
    }
    if let Some(home) = env::var_os("HOME") {
        return PathBuf::from(home).join(".wanwan-cloud-desktop");
    }
    PathBuf::from(".").join(".wanwan-cloud-desktop")
}

fn open_local_state_db() -> Result<Connection, String> {
    let state_dir = resolve_local_state_dir();
    fs::create_dir_all(&state_dir).map_err(|err| format!("创建本地状态目录失败: {}", err))?;
    let db_path = state_dir.join("client_state.db");
    let conn = Connection::open(db_path).map_err(|err| format!("打开本地状态数据库失败: {}", err))?;
    conn.execute(
        "CREATE TABLE IF NOT EXISTS login_state (
            id INTEGER PRIMARY KEY CHECK (id = 1),
            base_url TEXT NOT NULL,
            username TEXT NOT NULL,
            password TEXT NOT NULL,
            updated_at INTEGER NOT NULL
        )",
        [],
    )
    .map_err(|err| format!("初始化本地状态表失败: {}", err))?;
    Ok(conn)
}

fn save_login_state_record(base_url: &str, username: &str, password: &str) -> Result<(), String> {
    let conn = open_local_state_db()?;
    let encoded_password = encode_login_password(password)?;
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs() as i64)
        .unwrap_or_default();
    conn.execute(
        "INSERT INTO login_state (id, base_url, username, password, updated_at)
         VALUES (1, ?1, ?2, ?3, ?4)
         ON CONFLICT(id) DO UPDATE SET
           base_url = excluded.base_url,
           username = excluded.username,
           password = excluded.password,
           updated_at = excluded.updated_at",
        params![base_url, username, encoded_password, now],
    )
    .map_err(|err| format!("保存登录状态失败: {}", err))?;
    Ok(())
}

fn load_login_state_record() -> Result<Option<(String, String, String)>, String> {
    let conn = open_local_state_db()?;
    let mut stmt = conn
        .prepare("SELECT base_url, username, password FROM login_state WHERE id = 1 LIMIT 1")
        .map_err(|err| format!("读取登录状态失败: {}", err))?;
    let row = stmt.query_row([], |record| {
        Ok((
            record.get::<_, String>(0)?,
            record.get::<_, String>(1)?,
            record.get::<_, String>(2)?,
        ))
    });
    drop(stmt);
    match row {
        Ok((base_url, username, password)) => {
            match decode_login_password(&password) {
                Ok(decoded_password) => Ok(Some((base_url, username, decoded_password))),
                Err(err) => {
                    eprintln!("decode login state failed, clearing invalid state: {}", err);
                    let _ = conn.execute("DELETE FROM login_state WHERE id = 1", []);
                    Ok(None)
                }
            }
        }
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
        Err(err) => Err(format!("读取登录状态失败: {}", err)),
    }
}

fn clear_login_state_record() -> Result<(), String> {
    let conn = open_local_state_db()?;
    conn.execute("DELETE FROM login_state WHERE id = 1", [])
        .map_err(|err| format!("清除登录状态失败: {}", err))?;
    Ok(())
}

async fn parse_response_as_bridge(response: reqwest::Response) -> Result<BridgeResponse, String> {
    let status = response.status();
    let text = response
        .text()
        .await
        .map_err(|err| format!("读取响应失败: {}", err))?;
    let data = match serde_json::from_str::<Value>(&text) {
        Ok(parsed) => parsed,
        Err(_) => fallback_json(status, &text),
    };

    Ok(BridgeResponse {
        ok: status.is_success(),
        status: status.as_u16(),
        data,
    })
}

async fn request_json(
    client: &reqwest::Client,
    method: Method,
    url: String,
    body: Option<Value>,
    csrf_token: Option<String>,
) -> Result<BridgeResponse, String> {
    if url.is_empty() {
        return Err("API 地址不能为空".to_string());
    }

    let mut request = client
        .request(method, &url)
        .header("Accept", "application/json")
        .header("Content-Type", "application/json");

    if let Some(csrf) = csrf_token {
        request = request.header("X-CSRF-Token", csrf);
    }

    if let Some(payload) = body {
        request = request.json(&payload);
    }

    let response = request
        .send()
        .await
        .map_err(|err| format!("请求失败: {}", err))?;

    let status = response.status();
    let text = response
        .text()
        .await
        .map_err(|err| format!("读取响应失败: {}", err))?;
    let data = match serde_json::from_str::<Value>(&text) {
        Ok(parsed) => parsed,
        Err(_) => fallback_json(status, &text),
    };

    Ok(BridgeResponse {
        ok: status.is_success(),
        status: status.as_u16(),
        data,
    })
}

async fn fetch_csrf_token(client: &reqwest::Client, base_url: &str) -> Result<Option<String>, String> {
    let response = request_json(
        client,
        Method::GET,
        join_api_url(base_url, "/api/csrf-token"),
        None,
        None,
    )
    .await?;

    let token = response
        .data
        .get("csrfToken")
        .and_then(Value::as_str)
        .map(|v| v.to_string());

    Ok(token)
}

async fn request_with_optional_csrf(
    client: &reqwest::Client,
    method: Method,
    base_url: &str,
    path: &str,
    body: Option<Value>,
    need_csrf: bool,
) -> Result<BridgeResponse, String> {
    let csrf_token = if need_csrf {
        fetch_csrf_token(client, base_url).await?
    } else {
        None
    };

    request_json(
        client,
        method,
        join_api_url(base_url, path),
        body,
        csrf_token,
    )
    .await
}

#[tauri::command]
async fn api_login(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    username: String,
    password: String,
    captcha: Option<String>,
) -> Result<BridgeResponse, String> {
    let (platform, device_name, device_id) = build_desktop_client_meta();
    let mut body = Map::new();
    body.insert("username".to_string(), Value::String(username));
    body.insert("password".to_string(), Value::String(password));
    body.insert("client_type".to_string(), Value::String("desktop".to_string()));
    body.insert("platform".to_string(), Value::String(platform));
    body.insert("device_name".to_string(), Value::String(device_name));
    body.insert("device_id".to_string(), Value::String(device_id));
    if let Some(value) = captcha {
        if !value.trim().is_empty() {
            body.insert("captcha".to_string(), Value::String(value));
        }
    }

    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/login",
        Some(Value::Object(body)),
        false,
    )
    .await
}

#[tauri::command]
async fn api_get_profile(
    state: tauri::State<'_, ApiState>,
    base_url: String,
) -> Result<BridgeResponse, String> {
    request_with_optional_csrf(
        &state.client,
        Method::GET,
        &base_url,
        "/api/user/profile",
        None,
        false,
    )
    .await
}

#[tauri::command]
async fn api_list_online_devices(
    state: tauri::State<'_, ApiState>,
    base_url: String,
) -> Result<BridgeResponse, String> {
    request_with_optional_csrf(
        &state.client,
        Method::GET,
        &base_url,
        "/api/user/online-devices",
        None,
        false,
    )
    .await
}

#[tauri::command]
async fn api_kick_online_device(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    session_id: String,
) -> Result<BridgeResponse, String> {
    let session = session_id.trim().to_string();
    if session.is_empty() {
        return Err("会话标识不能为空".to_string());
    }
    if session.len() > 128 {
        return Err("会话标识长度无效".to_string());
    }
    let api_path = format!(
        "/api/user/online-devices/{}/kick",
        urlencoding::encode(&session)
    );
    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        &api_path,
        Some(Value::Object(Map::new())),
        true,
    )
    .await
}

#[tauri::command]
fn api_save_login_state(
    base_url: String,
    username: String,
    password: String,
) -> Result<BridgeResponse, String> {
    let normalized_base = normalize_base_url(&base_url);
    let normalized_user = username.trim().to_string();
    if normalized_base.is_empty() {
        return Err("服务地址不能为空".to_string());
    }
    if normalized_user.is_empty() {
        return Err("用户名不能为空".to_string());
    }
    if password.trim().is_empty() {
        return Err("密码不能为空".to_string());
    }

    save_login_state_record(&normalized_base, &normalized_user, &password)?;

    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert("message".to_string(), Value::String("登录状态已保存".to_string()));
    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
fn api_load_login_state() -> Result<BridgeResponse, String> {
    let state = load_login_state_record()?;
    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    if let Some((base_url, username, password)) = state {
        data.insert("hasState".to_string(), Value::Bool(true));
        data.insert("baseUrl".to_string(), Value::String(base_url));
        data.insert("username".to_string(), Value::String(username));
        data.insert("password".to_string(), Value::String(password));
    } else {
        data.insert("hasState".to_string(), Value::Bool(false));
    }

    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
fn api_clear_login_state() -> Result<BridgeResponse, String> {
    clear_login_state_record()?;
    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert("message".to_string(), Value::String("登录状态已清除".to_string()));
    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
async fn api_list_files(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    path: String,
) -> Result<BridgeResponse, String> {
    let normalized = if path.trim().is_empty() {
        "/".to_string()
    } else {
        path
    };
    let encoded = urlencoding::encode(&normalized);
    let api_url = format!("{}?path={}", join_api_url(&base_url, "/api/files"), encoded);

    request_json(&state.client, Method::GET, api_url, None, None).await
}

#[tauri::command]
async fn api_logout(
    state: tauri::State<'_, ApiState>,
    base_url: String,
) -> Result<BridgeResponse, String> {
    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/logout",
        Some(Value::Object(Map::new())),
        true,
    )
    .await
}

#[tauri::command]
async fn api_refresh_token(
    state: tauri::State<'_, ApiState>,
    base_url: String,
) -> Result<BridgeResponse, String> {
    request_json(
        &state.client,
        Method::POST,
        join_api_url(&base_url, "/api/refresh-token"),
        Some(Value::Object(Map::new())),
        None,
    )
    .await
}

#[tauri::command]
async fn api_search_files(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    path: String,
    keyword: String,
    search_type: Option<String>,
    limit: Option<u32>,
) -> Result<BridgeResponse, String> {
    let normalized_path = if path.trim().is_empty() {
        "/".to_string()
    } else {
        path
    };
    let kind = search_type
        .unwrap_or_else(|| "all".to_string())
        .trim()
        .to_string();
    let max_limit = limit.unwrap_or(100).clamp(1, 500);

    let api_url = format!(
        "{}?path={}&keyword={}&type={}&limit={}",
        join_api_url(&base_url, "/api/files/search"),
        urlencoding::encode(&normalized_path),
        urlencoding::encode(&keyword),
        urlencoding::encode(&kind),
        max_limit
    );

    request_json(&state.client, Method::GET, api_url, None, None).await
}

#[tauri::command]
async fn api_mkdir(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    path: String,
    folder_name: String,
) -> Result<BridgeResponse, String> {
    let mut body = Map::new();
    body.insert("path".to_string(), Value::String(path));
    body.insert("folderName".to_string(), Value::String(folder_name));

    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/files/mkdir",
        Some(Value::Object(body)),
        true,
    )
    .await
}

#[tauri::command]
async fn api_rename_file(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    path: String,
    old_name: String,
    new_name: String,
) -> Result<BridgeResponse, String> {
    let mut body = Map::new();
    body.insert("path".to_string(), Value::String(path));
    body.insert("oldName".to_string(), Value::String(old_name));
    body.insert("newName".to_string(), Value::String(new_name));

    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/files/rename",
        Some(Value::Object(body)),
        true,
    )
    .await
}

#[tauri::command]
async fn api_delete_file(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    path: String,
    file_name: String,
) -> Result<BridgeResponse, String> {
    let mut body = Map::new();
    body.insert("path".to_string(), Value::String(path));
    body.insert("fileName".to_string(), Value::String(file_name));

    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/files/delete",
        Some(Value::Object(body)),
        true,
    )
    .await
}

#[tauri::command]
async fn api_get_download_url(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    path: String,
    mode: Option<String>,
) -> Result<BridgeResponse, String> {
    let normalized_mode = mode.unwrap_or_else(|| "download".to_string());
    let api_url = format!(
        "{}?path={}&mode={}",
        join_api_url(&base_url, "/api/files/download-url"),
        urlencoding::encode(&path),
        urlencoding::encode(&normalized_mode)
    );
    request_json(&state.client, Method::GET, api_url, None, None).await
}

#[tauri::command]
async fn api_get_my_shares(
    state: tauri::State<'_, ApiState>,
    base_url: String,
) -> Result<BridgeResponse, String> {
    request_with_optional_csrf(
        &state.client,
        Method::GET,
        &base_url,
        "/api/share/my",
        None,
        false,
    )
    .await
}

#[tauri::command]
async fn api_create_share(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    share_type: String,
    file_path: String,
    file_name: Option<String>,
    password: Option<String>,
    expiry_days: Option<i32>,
) -> Result<BridgeResponse, String> {
    let mut body = Map::new();
    body.insert("share_type".to_string(), Value::String(share_type));
    body.insert("file_path".to_string(), Value::String(file_path));

    if let Some(name) = file_name {
        if !name.trim().is_empty() {
            body.insert("file_name".to_string(), Value::String(name.trim().to_string()));
        }
    }

    if let Some(raw_password) = password {
        let value = raw_password.trim();
        if value.is_empty() {
            body.insert("password".to_string(), Value::Null);
        } else {
            body.insert("password".to_string(), Value::String(value.to_string()));
        }
    }

    if let Some(days) = expiry_days {
        if days > 0 {
            body.insert("expiry_days".to_string(), Value::Number(days.into()));
        } else {
            body.insert("expiry_days".to_string(), Value::Null);
        }
    } else {
        body.insert("expiry_days".to_string(), Value::Null);
    }

    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/share/create",
        Some(Value::Object(body)),
        true,
    )
    .await
}

#[tauri::command]
async fn api_delete_share(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    share_id: u64,
) -> Result<BridgeResponse, String> {
    if share_id == 0 {
        return Err("无效的分享ID".to_string());
    }

    let csrf_token = fetch_csrf_token(&state.client, &base_url).await?;
    let path = format!("/api/share/{}", share_id);
    request_json(
        &state.client,
        Method::DELETE,
        join_api_url(&base_url, &path),
        None,
        csrf_token,
    )
    .await
}

#[tauri::command]
async fn api_create_direct_link(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    file_path: String,
    file_name: Option<String>,
    expiry_days: Option<i32>,
) -> Result<BridgeResponse, String> {
    let mut body = Map::new();
    body.insert("file_path".to_string(), Value::String(file_path));

    if let Some(name) = file_name {
        if !name.trim().is_empty() {
            body.insert("file_name".to_string(), Value::String(name.trim().to_string()));
        }
    }

    if let Some(days) = expiry_days {
        if days > 0 {
            body.insert("expiry_days".to_string(), Value::Number(days.into()));
        } else {
            body.insert("expiry_days".to_string(), Value::Null);
        }
    } else {
        body.insert("expiry_days".to_string(), Value::Null);
    }

    request_with_optional_csrf(
        &state.client,
        Method::POST,
        &base_url,
        "/api/direct-link/create",
        Some(Value::Object(body)),
        true,
    )
    .await
}

#[tauri::command]
async fn api_native_download(
    state: tauri::State<'_, ApiState>,
    window: tauri::WebviewWindow,
    url: String,
    file_name: Option<String>,
    task_id: Option<String>,
) -> Result<BridgeResponse, String> {
    let trimmed_url = url.trim().to_string();
    if trimmed_url.is_empty() {
        return Err("下载地址不能为空".to_string());
    }

    let preferred_name = file_name
        .as_deref()
        .map(|name| name.trim())
        .filter(|name| !name.is_empty())
        .unwrap_or("download.bin");

    let download_dir = resolve_download_dir();
    if !download_dir.exists() {
        fs::create_dir_all(&download_dir)
            .map_err(|err| format!("创建下载目录失败: {}", err))?;
    }

    let resume_temp_path = build_download_resume_temp_path(&download_dir, preferred_name, &trimmed_url);
    let existing_size = if resume_temp_path.exists() {
        fs::metadata(&resume_temp_path)
            .ok()
            .map(|meta| meta.len())
            .unwrap_or(0)
    } else {
        0
    };

    let mut request = state.client.get(&trimmed_url);
    if existing_size > 0 {
        request = request.header("Range", format!("bytes={}-", existing_size));
    }

    let response = request
        .send()
        .await
        .map_err(|err| format!("下载请求失败: {}", err))?;
    let status = response.status();

    if status == reqwest::StatusCode::RANGE_NOT_SATISFIABLE && existing_size > 0 {
        let save_path = alloc_download_path(&download_dir, preferred_name);
        fs::rename(&resume_temp_path, &save_path)
            .map_err(|err| format!("完成断点下载失败: {}", err))?;
        if let Some(saved_name) = save_path.file_name().and_then(|name| name.to_str()) {
            if is_update_installer_file_name(saved_name) {
                let _ = cleanup_old_update_installers(&download_dir, saved_name, 3);
            }
        }
        if let Some(ref id) = task_id {
            emit_native_download_progress(
                &window,
                id,
                existing_size,
                Some(existing_size),
                existing_size,
                true,
            );
        }
        let mut data = Map::new();
        data.insert("success".to_string(), Value::Bool(true));
        data.insert(
            "savePath".to_string(),
            Value::String(save_path.to_string_lossy().to_string()),
        );
        data.insert(
            "downloadedBytes".to_string(),
            Value::Number(serde_json::Number::from(existing_size)),
        );
        data.insert(
            "resumedBytes".to_string(),
            Value::Number(serde_json::Number::from(existing_size)),
        );
        return Ok(BridgeResponse {
            ok: true,
            status: 200,
            data: Value::Object(data),
        });
    }

    if !status.is_success() {
        return Ok(BridgeResponse {
            ok: false,
            status: status.as_u16(),
            data: fallback_json(status, "下载失败"),
        });
    }

    let append_mode = existing_size > 0 && status == reqwest::StatusCode::PARTIAL_CONTENT;
    let total_bytes = if append_mode {
        response
            .content_length()
            .map(|remaining| remaining.saturating_add(existing_size))
    } else {
        response.content_length()
    };
    let resumed_bytes = if append_mode { existing_size } else { 0 };

    if let Some(ref id) = task_id {
        emit_native_download_progress(
            &window,
            id,
            if append_mode { existing_size } else { 0 },
            total_bytes,
            resumed_bytes,
            false,
        );
    }

    if !append_mode && resume_temp_path.exists() {
        fs::remove_file(&resume_temp_path)
            .map_err(|err| format!("重置断点下载文件失败: {}", err))?;
    }

    let mut target_file = fs::OpenOptions::new()
        .create(true)
        .write(true)
        .append(append_mode)
        .truncate(!append_mode)
        .open(&resume_temp_path)
        .map_err(|err| format!("创建文件失败: {}", err))?;

    let mut downloaded_bytes: u64 = if append_mode { existing_size } else { 0 };
    let mut stream = response;
    let mut last_emit = Instant::now();
    while let Some(chunk) = stream
        .chunk()
        .await
        .map_err(|err| format!("读取下载流失败: {}", err))?
    {
        target_file
            .write_all(&chunk)
            .map_err(|err| format!("写入文件失败: {}", err))?;
        downloaded_bytes += chunk.len() as u64;

        if let Some(ref id) = task_id {
            if last_emit.elapsed() >= Duration::from_millis(120) {
                emit_native_download_progress(
                    &window,
                    id,
                    downloaded_bytes,
                    total_bytes,
                    resumed_bytes,
                    false,
                );
                last_emit = Instant::now();
            }
        }
    }

    target_file
        .flush()
        .map_err(|err| format!("刷新文件失败: {}", err))?;

    let save_path = alloc_download_path(&download_dir, preferred_name);
    fs::rename(&resume_temp_path, &save_path)
        .map_err(|err| format!("保存下载文件失败: {}", err))?;
    if let Some(saved_name) = save_path.file_name().and_then(|name| name.to_str()) {
        if is_update_installer_file_name(saved_name) {
            let _ = cleanup_old_update_installers(&download_dir, saved_name, 3);
        }
    }

    if let Some(ref id) = task_id {
        emit_native_download_progress(
            &window,
            id,
            downloaded_bytes,
            total_bytes.or(Some(downloaded_bytes)),
            resumed_bytes,
            true,
        );
    }

    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert(
        "savePath".to_string(),
        Value::String(save_path.to_string_lossy().to_string()),
    );
    data.insert(
        "downloadedBytes".to_string(),
        Value::Number(serde_json::Number::from(downloaded_bytes)),
    );
    data.insert(
        "resumedBytes".to_string(),
        Value::Number(serde_json::Number::from(if append_mode { existing_size } else { 0 })),
    );

    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
fn api_compute_file_sha256(file_path: String) -> Result<BridgeResponse, String> {
    let normalized = file_path.trim().to_string();
    if normalized.is_empty() {
        return Err("文件路径不能为空".to_string());
    }
    let target = PathBuf::from(&normalized);
    if !target.exists() {
        return Err("文件不存在".to_string());
    }
    if !target.is_file() {
        return Err("无效的文件路径".to_string());
    }
    let file_size = fs::metadata(&target)
        .map(|meta| meta.len())
        .map_err(|err| format!("读取文件大小失败: {}", err))?;
    let sha256 = compute_file_sha256_hex(&target)?;

    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert("filePath".to_string(), Value::String(normalized));
    data.insert("sha256".to_string(), Value::String(sha256));
    data.insert(
        "fileSize".to_string(),
        Value::Number(serde_json::Number::from(file_size)),
    );

    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
fn api_launch_installer(installer_path: String) -> Result<BridgeResponse, String> {
    let path_text = installer_path.trim().to_string();
    if path_text.is_empty() {
        return Err("安装包路径不能为空".to_string());
    }

    let installer = PathBuf::from(&path_text);
    if !installer.exists() {
        return Err("安装包不存在，请重新下载".to_string());
    }
    if !installer.is_file() {
        return Err("安装包路径无效".to_string());
    }

    #[cfg(target_os = "windows")]
    let spawn_result = Command::new(&installer).spawn();

    #[cfg(target_os = "macos")]
    let spawn_result = Command::new("open").arg(&installer).spawn();

    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
    let spawn_result = Command::new("xdg-open").arg(&installer).spawn();

    spawn_result.map_err(|err| format!("启动安装程序失败: {}", err))?;

    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert("message".to_string(), Value::String("安装程序已启动".to_string()));
    data.insert("installerPath".to_string(), Value::String(path_text));

    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
fn api_silent_install_and_restart(installer_path: String) -> Result<BridgeResponse, String> {
    let path_text = installer_path.trim().to_string();
    if path_text.is_empty() {
        return Err("安装包路径不能为空".to_string());
    }

    let installer = PathBuf::from(&path_text);
    if !installer.exists() {
        return Err("安装包不存在，请重新下载".to_string());
    }
    if !installer.is_file() {
        return Err("安装包路径无效".to_string());
    }

    #[cfg(target_os = "windows")]
    let windows_log_file_path: String;
    #[cfg(target_os = "windows")]
    let windows_script_file_path: String;

    #[cfg(target_os = "windows")]
    {
        let current_exe = env::current_exe().map_err(|err| format!("获取当前程序路径失败: {}", err))?;
        let current_pid = std::process::id();
        let temp_dir = env::temp_dir().join("wanwan-cloud-desktop");
        fs::create_dir_all(&temp_dir).map_err(|err| format!("创建更新脚本目录失败: {}", err))?;
        let script_stamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|duration| duration.as_millis())
            .unwrap_or_default();
        let script_path = temp_dir.join(format!("silent-update-{}.cmd", script_stamp));
        let log_path = temp_dir.join(format!("silent-update-{}.log", script_stamp));
        windows_log_file_path = log_path.to_string_lossy().to_string();
        windows_script_file_path = script_path.to_string_lossy().to_string();

        let installer_text = installer.to_string_lossy().replace('"', "\"\"");
        let app_text = current_exe.to_string_lossy().replace('"', "\"\"");
        let log_text = log_path.to_string_lossy().replace('"', "\"\"");
        let bootstrap_content = format!(
            "[bootstrap] silent updater prepared\r\npid={}\r\ninstaller={}\r\nscript={}\r\n",
            current_pid,
            installer.to_string_lossy(),
            script_path.to_string_lossy()
        );
        fs::write(&log_path, bootstrap_content).map_err(|err| format!("写入更新日志失败: {}", err))?;
        let script_content = format!(
            "@echo off\r\n\
setlocal enableextensions\r\n\
set \"INSTALLER={installer}\"\r\n\
set \"APP_EXE={app_exe}\"\r\n\
set \"APP_PID={app_pid}\"\r\n\
set \"LOG_FILE={log_file}\"\r\n\
echo [%%date%% %%time%%] update script started > \"%LOG_FILE%\"\r\n\
if not exist \"%INSTALLER%\" (\r\n\
  echo [%%date%% %%time%%] installer not found: %INSTALLER% >> \"%LOG_FILE%\"\r\n\
  exit /b 2\r\n\
)\r\n\
timeout /t 1 /nobreak >nul\r\n\
taskkill /PID %APP_PID% /F >nul 2>nul\r\n\
timeout /t 1 /nobreak >nul\r\n\
start \"\" /wait \"%INSTALLER%\" /S\r\n\
set \"INSTALL_EXIT=%ERRORLEVEL%\"\r\n\
echo [%%date%% %%time%%] installer exit code: %INSTALL_EXIT% >> \"%LOG_FILE%\"\r\n\
set \"RETRY_COUNT=0\"\r\n\
:wait_for_app\r\n\
if exist \"%APP_EXE%\" goto launch_app\r\n\
if exist \"%LOCALAPPDATA%\\玩玩云\\desktop-client.exe\" (\r\n\
  set \"APP_EXE=%LOCALAPPDATA%\\玩玩云\\desktop-client.exe\"\r\n\
  goto launch_app\r\n\
)\r\n\
if %RETRY_COUNT% GEQ 25 goto app_missing\r\n\
set /a RETRY_COUNT+=1\r\n\
timeout /t 1 /nobreak >nul\r\n\
goto wait_for_app\r\n\
:launch_app\r\n\
start \"\" \"%APP_EXE%\"\r\n\
set \"START_EXIT=%ERRORLEVEL%\"\r\n\
echo [%%date%% %%time%%] launch app exit code: %START_EXIT% path=%APP_EXE% >> \"%LOG_FILE%\"\r\n\
goto cleanup\r\n\
:app_missing\r\n\
echo [%%date%% %%time%%] app exe not found after install >> \"%LOG_FILE%\"\r\n\
:cleanup\r\n\
del \"%~f0\" >nul 2>nul\r\n",
            installer = installer_text,
            app_exe = app_text,
            app_pid = current_pid,
            log_file = log_text
        );
        fs::write(&script_path, script_content).map_err(|err| format!("写入更新脚本失败: {}", err))?;

        let mut updater_cmd = Command::new("cmd");
        let spawn_result = updater_cmd
            .arg("/D")
            .arg("/C")
            .arg("call")
            .arg(&script_path)
            .current_dir(&temp_dir)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
            .spawn();
        if let Err(err) = spawn_result {
            let _ = fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open(&log_path)
                .and_then(|mut file| {
                    writeln!(file, "[bootstrap] failed to spawn updater cmd: {}", err)?;
                    Ok(())
                });
            return Err(format!("启动静默更新流程失败: {}", err));
        }

        let mut cleanup_entries: Vec<PathBuf> = fs::read_dir(&temp_dir)
            .ok()
            .into_iter()
            .flat_map(|entries| entries.filter_map(Result::ok))
            .map(|entry| entry.path())
            .filter(|path| {
                path.file_name()
                    .and_then(|name| name.to_str())
                    .map(|name| name.starts_with("silent-update-"))
                    .unwrap_or(false)
            })
            .collect();
        cleanup_entries.sort_by_key(|path| {
            fs::metadata(path)
                .ok()
                .and_then(|meta| meta.modified().ok())
                .unwrap_or(SystemTime::UNIX_EPOCH)
        });
        if cleanup_entries.len() > 24 {
            let remove_count = cleanup_entries.len().saturating_sub(24);
            for stale_path in cleanup_entries.into_iter().take(remove_count) {
                let _ = fs::remove_file(stale_path);
            }
        }
    }

    #[cfg(not(target_os = "windows"))]
    {
        #[cfg(target_os = "macos")]
        let spawn_result = Command::new("open").arg(&installer).spawn();
        #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
        let spawn_result = Command::new("xdg-open").arg(&installer).spawn();
        spawn_result.map_err(|err| format!("启动安装程序失败: {}", err))?;
    }

    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert(
        "message".to_string(),
        Value::String("静默安装流程已启动，安装完成后将自动重启".to_string()),
    );
    data.insert("installerPath".to_string(), Value::String(path_text));
    #[cfg(target_os = "windows")]
    {
        data.insert(
            "logPath".to_string(),
            Value::String(
                env::temp_dir()
                    .join("wanwan-cloud-desktop")
                    .to_string_lossy()
                    .to_string(),
            ),
        );
        data.insert(
            "logFilePath".to_string(),
            Value::String(windows_log_file_path),
        );
        data.insert(
            "scriptPath".to_string(),
            Value::String(windows_script_file_path),
        );
    }

    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
async fn api_check_client_update(
    state: tauri::State<'_, ApiState>,
    base_url: String,
    current_version: String,
    platform: Option<String>,
    channel: Option<String>,
) -> Result<BridgeResponse, String> {
    let normalized_platform = platform
        .unwrap_or_else(|| "windows-x64".to_string())
        .trim()
        .to_string();
    let normalized_channel = channel
        .unwrap_or_else(|| "stable".to_string())
        .trim()
        .to_string();

    let api_url = format!(
        "{}?currentVersion={}&platform={}&channel={}",
        join_api_url(&base_url, "/api/client/desktop-update"),
        urlencoding::encode(current_version.trim()),
        urlencoding::encode(&normalized_platform),
        urlencoding::encode(&normalized_channel)
    );
    request_json(&state.client, Method::GET, api_url, None, None).await
}

#[tauri::command]
async fn api_list_local_files(dir_path: String) -> Result<BridgeResponse, String> {
    let trimmed = dir_path.trim().to_string();
    if trimmed.is_empty() {
        return Err("本地目录不能为空".to_string());
    }

    let root = PathBuf::from(&trimmed);
    if !root.exists() {
        return Err("本地目录不存在".to_string());
    }
    if !root.is_dir() {
        return Err("请选择有效的目录路径".to_string());
    }

    let mut items: Vec<Value> = Vec::new();
    for entry in walkdir::WalkDir::new(&root)
        .follow_links(false)
        .into_iter()
        .filter_map(Result::ok)
    {
        if !entry.file_type().is_file() {
            continue;
        }

        let full_path = entry.path();
        let relative = full_path.strip_prefix(&root).unwrap_or(full_path);
        let relative_path = relative.to_string_lossy().replace('\\', "/");
        if relative_path.trim().is_empty() {
            continue;
        }

        let metadata = match entry.metadata() {
            Ok(meta) => meta,
            Err(_) => continue,
        };
        let modified_ms_u128 = metadata
            .modified()
            .ok()
            .and_then(|value| value.duration_since(UNIX_EPOCH).ok())
            .map(|duration| duration.as_millis())
            .unwrap_or(0);
        let modified_ms = std::cmp::min(modified_ms_u128, u128::from(u64::MAX)) as u64;

        let mut row = Map::new();
        row.insert(
            "path".to_string(),
            Value::String(full_path.to_string_lossy().to_string()),
        );
        row.insert("relativePath".to_string(), Value::String(relative_path));
        row.insert(
            "size".to_string(),
            Value::Number(serde_json::Number::from(metadata.len())),
        );
        row.insert(
            "modifiedMs".to_string(),
            Value::Number(serde_json::Number::from(modified_ms)),
        );
        items.push(Value::Object(row));
    }

    items.sort_by(|a, b| {
        let av = a
            .get("relativePath")
            .and_then(Value::as_str)
            .unwrap_or_default();
        let bv = b
            .get("relativePath")
            .and_then(Value::as_str)
            .unwrap_or_default();
        av.cmp(bv)
    });

    let mut data = Map::new();
    data.insert("success".to_string(), Value::Bool(true));
    data.insert("rootPath".to_string(), Value::String(trimmed));
    data.insert(
        "count".to_string(),
        Value::Number(serde_json::Number::from(items.len() as u64)),
    );
    data.insert("items".to_string(), Value::Array(items));

    Ok(BridgeResponse {
        ok: true,
        status: 200,
        data: Value::Object(data),
    })
}

#[tauri::command]
async fn api_upload_file_resumable(
    state: tauri::State<'_, ApiState>,
    window: tauri::WebviewWindow,
    base_url: String,
    file_path: String,
    target_path: String,
    chunk_size: Option<u64>,
    task_id: Option<String>,
) -> Result<BridgeResponse, String> {
    let trimmed_path = file_path.trim().to_string();
    if trimmed_path.is_empty() {
        return Err("上传文件路径不能为空".to_string());
    }

    let source_path = PathBuf::from(&trimmed_path);
    if !source_path.exists() {
        return Err("上传文件不存在".to_string());
    }
    if !source_path.is_file() {
        return Err("仅支持上传文件，不支持文件夹".to_string());
    }

    let metadata = fs::metadata(&source_path).map_err(|err| format!("读取文件信息失败: {}", err))?;
    let file_size = metadata.len();
    if file_size == 0 {
        return Err("空文件不支持分片上传".to_string());
    }
    let file_fingerprint = build_upload_file_fingerprint(&metadata);

    let file_name = source_path
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| name.to_string())
        .ok_or_else(|| "无法识别文件名".to_string())?;
    let normalized_target = if target_path.trim().is_empty() {
        "/".to_string()
    } else {
        target_path
    };
    let effective_chunk = chunk_size.unwrap_or(4 * 1024 * 1024).clamp(256 * 1024, 32 * 1024 * 1024);

    let csrf_token = fetch_csrf_token(&state.client, &base_url).await?;
    let mut init_body = Map::new();
    init_body.insert("filename".to_string(), Value::String(file_name.clone()));
    init_body.insert("path".to_string(), Value::String(normalized_target));
    init_body.insert(
        "size".to_string(),
        Value::Number(serde_json::Number::from(file_size)),
    );
    init_body.insert(
        "chunk_size".to_string(),
        Value::Number(serde_json::Number::from(effective_chunk)),
    );
    if let Some(hash) = file_fingerprint.clone() {
        init_body.insert("file_hash".to_string(), Value::String(hash));
    }

    let init_resp = request_json(
        &state.client,
        Method::POST,
        join_api_url(&base_url, "/api/upload/resumable/init"),
        Some(Value::Object(init_body)),
        csrf_token.clone(),
    )
    .await?;

    if !init_resp.ok || !init_resp.data.get("success").and_then(Value::as_bool).unwrap_or(false) {
        return Ok(init_resp);
    }

    let session_id = init_resp
        .data
        .get("session_id")
        .and_then(Value::as_str)
        .map(|v| v.trim().to_string())
        .filter(|v| !v.is_empty())
        .ok_or_else(|| "分片上传会话创建失败".to_string())?;
    let server_chunk_size = init_resp
        .data
        .get("chunk_size")
        .and_then(Value::as_u64)
        .unwrap_or(effective_chunk)
        .max(1);
    let total_chunks = init_resp
        .data
        .get("total_chunks")
        .and_then(Value::as_u64)
        .unwrap_or_else(|| ((file_size + server_chunk_size - 1) / server_chunk_size).max(1));
    let uploaded_chunks: std::collections::HashSet<u64> = init_resp
        .data
        .get("uploaded_chunks")
        .and_then(Value::as_array)
        .map(|arr| {
            arr.iter()
                .filter_map(Value::as_u64)
                .collect::<std::collections::HashSet<u64>>()
        })
        .unwrap_or_default();

    let mut uploaded_bytes = uploaded_chunks.iter().fold(0_u64, |sum, chunk_index| {
        let offset = chunk_index.saturating_mul(server_chunk_size);
        let remaining = file_size.saturating_sub(offset);
        let bytes = std::cmp::min(remaining, server_chunk_size);
        sum.saturating_add(bytes)
    });
    if let Some(ref id) = task_id {
        emit_native_upload_progress(&window, id, uploaded_bytes, file_size, false);
    }

    let mut source = fs::File::open(&source_path).map_err(|err| format!("打开文件失败: {}", err))?;
    let mut last_emit = Instant::now();
    for chunk_index in 0..total_chunks {
        if uploaded_chunks.contains(&chunk_index) {
            continue;
        }

        let offset = chunk_index * server_chunk_size;
        let remaining = file_size.saturating_sub(offset);
        if remaining == 0 {
            break;
        }
        let read_size = std::cmp::min(remaining, server_chunk_size) as usize;

        source
            .seek(SeekFrom::Start(offset))
            .map_err(|err| format!("读取分片失败: {}", err))?;
        let mut buf = vec![0_u8; read_size];
        source
            .read_exact(&mut buf)
            .map_err(|err| format!("读取分片失败: {}", err))?;

        let chunk_part_name = format!("{}.part{}", file_name, chunk_index);
        let mut chunk_done = false;
        for attempt in 0..=RESUMABLE_CHUNK_MAX_RETRIES {
            let multipart = reqwest::multipart::Form::new()
                .text("session_id", session_id.clone())
                .text("chunk_index", chunk_index.to_string())
                .part(
                    "chunk",
                    reqwest::multipart::Part::bytes(buf.clone()).file_name(chunk_part_name.clone()),
                );

            let mut request = state
                .client
                .post(join_api_url(&base_url, "/api/upload/resumable/chunk"))
                .header("Accept", "application/json")
                .timeout(Duration::from_secs(60 * 10))
                .multipart(multipart);
            if let Some(token) = csrf_token.clone() {
                request = request.header("X-CSRF-Token", token);
            }

            let chunk_bridge = match request.send().await {
                Ok(chunk_resp) => parse_response_as_bridge(chunk_resp).await?,
                Err(err) => {
                    if attempt < RESUMABLE_CHUNK_MAX_RETRIES && is_retryable_transport_error(&err) {
                        sleep(build_chunk_retry_delay(attempt)).await;
                        continue;
                    }
                    return Err(format!("上传分片失败: {}", err));
                }
            };

            let chunk_success = chunk_bridge.ok
                && chunk_bridge
                    .data
                    .get("success")
                    .and_then(Value::as_bool)
                    .unwrap_or(false);
            if chunk_success {
                chunk_done = true;
                break;
            }

            let message = chunk_bridge
                .data
                .get("message")
                .and_then(Value::as_str)
                .unwrap_or_default()
                .to_string();
            let retryable_status = is_retryable_upload_status(chunk_bridge.status);
            let retryable_message = message.contains("超时")
                || message.to_lowercase().contains("timeout")
                || message.contains("稍后重试");
            if attempt < RESUMABLE_CHUNK_MAX_RETRIES && (retryable_status || retryable_message) {
                sleep(build_chunk_retry_delay(attempt)).await;
                continue;
            }
            return Ok(chunk_bridge);
        }
        if !chunk_done {
            return Err("上传分片失败，请重试".to_string());
        }

        uploaded_bytes = uploaded_bytes.saturating_add(read_size as u64).min(file_size);
        if let Some(ref id) = task_id {
            if last_emit.elapsed() >= Duration::from_millis(120) {
                emit_native_upload_progress(&window, id, uploaded_bytes, file_size, false);
                last_emit = Instant::now();
            }
        }
    }

    let mut complete_body = Map::new();
    complete_body.insert("session_id".to_string(), Value::String(session_id));
    let mut complete_request = state
        .client
        .post(join_api_url(&base_url, "/api/upload/resumable/complete"))
        .header("Accept", "application/json")
        .header("Content-Type", "application/json")
        .timeout(Duration::from_secs(60 * 20))
        .json(&Value::Object(complete_body));
    if let Some(token) = csrf_token {
        complete_request = complete_request.header("X-CSRF-Token", token);
    }
    let complete_raw = complete_request
        .send()
        .await
        .map_err(|err| format!("完成分片上传失败: {}", err))?;
    let complete_resp = parse_response_as_bridge(complete_raw).await?;

    if complete_resp.ok && complete_resp.data.get("success").and_then(Value::as_bool).unwrap_or(false) {
        if let Some(ref id) = task_id {
            emit_native_upload_progress(&window, id, file_size, file_size, true);
        }
    }

    Ok(complete_resp)
}

#[tauri::command]
async fn api_upload_file(
    state: tauri::State<'_, ApiState>,
    window: tauri::WebviewWindow,
    base_url: String,
    file_path: String,
    target_path: String,
    task_id: Option<String>,
) -> Result<BridgeResponse, String> {
    let trimmed_path = file_path.trim().to_string();
    if trimmed_path.is_empty() {
        return Err("上传文件路径不能为空".to_string());
    }

    let source_path = PathBuf::from(trimmed_path);
    if !source_path.exists() {
        return Err("上传文件不存在".to_string());
    }
    if !source_path.is_file() {
        return Err("仅支持上传文件，不支持文件夹".to_string());
    }
    let file_meta = fs::metadata(&source_path).map_err(|err| format!("读取文件信息失败: {}", err))?;
    let file_size = file_meta.len();
    let file_fingerprint = build_upload_file_fingerprint(&file_meta);

    let file_name = source_path
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| name.to_string())
        .ok_or_else(|| "无法识别文件名".to_string())?;
    let normalized_target = if target_path.trim().is_empty() {
        "/".to_string()
    } else {
        target_path
    };

    let csrf_token = fetch_csrf_token(&state.client, &base_url).await?;
    let upload_url = join_api_url(&base_url, "/api/upload");
    if upload_url.trim().is_empty() {
        return Err("API 地址不能为空".to_string());
    }

    if let Some(ref id) = task_id {
        emit_native_upload_progress(&window, id, 0, file_size.max(1), false);
    }

    // 使用流式 multipart 上传，避免大文件整块读入内存导致占用暴涨。
    let file_part = reqwest::multipart::Part::file(&source_path)
        .await
        .map_err(|err| format!("读取文件失败: {}", err))?
        .file_name(file_name);

    let mut multipart = reqwest::multipart::Form::new()
        .text("path", normalized_target)
        .part("file", file_part);
    if let Some(hash) = file_fingerprint {
        multipart = multipart.text("file_hash", hash);
    }

    let mut request = state
        .client
        .post(&upload_url)
        .header("Accept", "application/json")
        .timeout(Duration::from_secs(60 * 30))
        .multipart(multipart);

    if let Some(csrf) = csrf_token {
        request = request.header("X-CSRF-Token", csrf);
    }

    let response = request
        .send()
        .await
        .map_err(|err| format!("上传请求失败: {}", err))?;

    let status = response.status();
    let text = response
        .text()
        .await
        .map_err(|err| format!("读取响应失败: {}", err))?;
    let data = match serde_json::from_str::<Value>(&text) {
        Ok(parsed) => parsed,
        Err(_) => fallback_json(status, &text),
    };
    let success = status.is_success() && data.get("success").and_then(Value::as_bool).unwrap_or(false);
    if success {
        if let Some(ref id) = task_id {
            emit_native_upload_progress(&window, id, file_size, file_size.max(1), true);
        }
    }

    Ok(BridgeResponse {
        ok: status.is_success(),
        status: status.as_u16(),
        data,
    })
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let client = reqwest::Client::builder()
        .cookie_store(true)
        .timeout(Duration::from_secs(90))
        .build()
        .expect("failed to build reqwest client");

    tauri::Builder::default()
        .manage(ApiState { client })
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![
            api_login,
            api_save_login_state,
            api_load_login_state,
            api_clear_login_state,
            api_get_profile,
            api_list_online_devices,
            api_kick_online_device,
            api_list_files,
            api_logout,
            api_refresh_token,
            api_search_files,
            api_mkdir,
            api_rename_file,
            api_delete_file,
            api_get_download_url,
            api_get_my_shares,
            api_create_share,
            api_delete_share,
            api_create_direct_link,
            api_native_download,
            api_compute_file_sha256,
            api_launch_installer,
            api_silent_install_and_restart,
            api_check_client_update,
            api_list_local_files,
            api_upload_file_resumable,
            api_upload_file
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
