use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};

/// 原子写：先写同目录临时文件，再 rename 覆盖目标。
/// 目的：① 并发读者（如运行中的 claude.exe 读 ~/.claude.json）永远读到完整的旧版或新版，
/// 绝不会读到 fs::write 截断到一半的残缺内容；② 写入途中崩溃只会留下临时文件，原文件完好。
/// 临时文件与目标同目录，保证 rename 在同一卷内（接近）原子完成。
pub fn write_atomic(path: &Path, contents: &[u8]) -> Result<(), String> {
    let parent = path.parent().ok_or("无法获取父目录")?;
    fs::create_dir_all(parent).map_err(|e| e.to_string())?;
    let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("ccgui");
    // 临时名带 pid + 时间戳，避免同进程/多进程并发写时撞名
    let tmp = parent.join(format!(
        ".{file_name}.tmp-{}-{}",
        std::process::id(),
        crate::store::now_ms()
    ));
    fs::write(&tmp, contents).map_err(|e| format!("写临时文件失败: {e}"))?;
    // Windows 的 fs::rename 用 MOVEFILE_REPLACE_EXISTING 覆盖已存在目标；
    // 若目标正被其他进程独占可能短暂失败，重试几次跨过其持锁高峰。
    const MAX_ATTEMPTS: u32 = 5;
    let mut last_err = String::new();
    for attempt in 0..MAX_ATTEMPTS {
        if attempt > 0 {
            std::thread::sleep(std::time::Duration::from_millis(20 * u64::from(attempt)));
        }
        match fs::rename(&tmp, path) {
            Ok(()) => return Ok(()),
            Err(e) => last_err = e.to_string(),
        }
    }
    let _ = fs::remove_file(&tmp); // 失败时清理临时文件，不留垃圾
    Err(format!("原子写失败(重试 {MAX_ATTEMPTS} 次仍失败): {last_err}"))
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FsEntry {
    pub name: String,
    pub path: String,
    pub is_dir: bool,
    /// 字节数；目录为 0
    pub size: u64,
    /// 修改时间（Unix 毫秒）；读取失败为 0
    pub mtime_ms: u64,
}

/// 列出目录直接子项：目录在前，名称不区分大小写排序（文件树按需逐层加载）
pub fn list_dir(path: &str) -> Result<Vec<FsEntry>, String> {
    let entries = fs::read_dir(path).map_err(|e| e.to_string())?;
    let mut out: Vec<FsEntry> = Vec::new();
    for entry in entries.flatten() {
        let p = entry.path();
        let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
        let (size, mtime_ms) = match entry.metadata() {
            Ok(m) => (
                if is_dir { 0 } else { m.len() },
                m.modified()
                    .ok()
                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
                    .map(|d| d.as_millis() as u64)
                    .unwrap_or(0),
            ),
            Err(_) => (0, 0),
        };
        out.push(FsEntry {
            name: entry.file_name().to_string_lossy().into_owned(),
            path: p.to_string_lossy().into_owned(),
            is_dir,
            size,
            mtime_ms,
        });
    }
    out.sort_by(|a, b| {
        b.is_dir
            .cmp(&a.is_dir)
            .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
    });
    Ok(out)
}

/// 目录浏览/搜索时默认跳过的重型噪音目录（移动端默认折叠，搜索直接不下钻）
pub const NOISE_DIRS: [&str; 8] = [
    ".git",
    "node_modules",
    "target",
    "dist",
    "build",
    "out",
    ".next",
    ".cache",
];

/// 列出目录浏览的起点：Windows 为各已存在盘符，其他系统为 /；末尾附用户主目录（去重）
pub fn list_roots() -> Vec<FsEntry> {
    let mut out: Vec<FsEntry> = Vec::new();
    #[cfg(windows)]
    for c in b'A'..=b'Z' {
        let p = format!("{}:\\", c as char);
        if fs::metadata(&p).is_ok() {
            out.push(FsEntry {
                name: p.clone(),
                path: p,
                is_dir: true,
                size: 0,
                mtime_ms: 0,
            });
        }
    }
    #[cfg(not(windows))]
    out.push(FsEntry {
        name: "/".into(),
        path: "/".into(),
        is_dir: true,
        size: 0,
        mtime_ms: 0,
    });
    if let Some(h) = dirs::home_dir() {
        let hp = h.to_string_lossy().into_owned();
        if !out.iter().any(|e| e.path == hp) {
            out.push(FsEntry {
                name: "主目录".into(),
                path: hp,
                is_dir: true,
                size: 0,
                mtime_ms: 0,
            });
        }
    }
    out
}

/// 移动端文件搜索：从 root 受限递归（跳过噪音目录与符号链接、限深、封顶），
/// 按文件名（不区分大小写）子串匹配，目录在前排序。结果截断上限，避免大包走中继。
pub fn search_files(root: &str, query: &str) -> Result<Vec<FsEntry>, String> {
    const MAX_DEPTH: usize = 10;
    const MAX_RESULTS: usize = 100;
    const MAX_VISITS: usize = 100_000;
    let q = query.trim().to_lowercase();
    if q.is_empty() {
        return Ok(Vec::new());
    }
    if !Path::new(root).is_dir() {
        return Err(format!("目录不存在: {root}"));
    }
    let mut out: Vec<FsEntry> = Vec::new();
    let mut stack: Vec<(PathBuf, usize)> = vec![(PathBuf::from(root), 0)];
    let mut visits = 0usize;
    while let Some((dir, depth)) = stack.pop() {
        if out.len() >= MAX_RESULTS || visits >= MAX_VISITS {
            break;
        }
        let rd = match fs::read_dir(&dir) {
            Ok(r) => r,
            Err(_) => continue, // 无权限等：跳过该子树，不整体失败
        };
        for entry in rd.flatten() {
            visits += 1;
            if visits >= MAX_VISITS || out.len() >= MAX_RESULTS {
                break;
            }
            let ft = match entry.file_type() {
                Ok(t) => t,
                Err(_) => continue,
            };
            if ft.is_symlink() {
                continue; // 不跟符号链接，防循环
            }
            let name = entry.file_name().to_string_lossy().into_owned();
            let is_dir = ft.is_dir();
            if is_dir {
                if NOISE_DIRS.contains(&name.to_lowercase().as_str()) {
                    continue;
                }
                if depth < MAX_DEPTH {
                    stack.push((entry.path(), depth + 1));
                }
            }
            if name.to_lowercase().contains(&q) {
                out.push(FsEntry {
                    name,
                    path: entry.path().to_string_lossy().into_owned(),
                    is_dir,
                    size: 0,
                    mtime_ms: 0,
                });
            }
        }
    }
    out.sort_by(|a, b| {
        b.is_dir
            .cmp(&a.is_dir)
            .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
    });
    Ok(out)
}

pub fn set_clipboard_text(text: &str) -> Result<(), String> {
    // Windows 剪贴板是全局独占锁:华为电脑管家(跨设备剪贴板)、ToDesk、搜狗输入法、
    // 微信/QQ 等监听者收到剪贴板更新通知后会立刻打开剪贴板读取,与我们的写入抢锁,
    // 单次 OpenClipboard 很容易失败(arboard 内置快速重试窗口仅约 10ms,扛不住联网
    // 同步类监听者更久的持锁)。这里加应用级重试 + 递增退避,跨过其持锁高峰。
    const MAX_ATTEMPTS: u32 = 8;
    let mut last_err = String::new();
    for attempt in 0..MAX_ATTEMPTS {
        // 首次立即尝试(成功零延迟);失败后退避 20/40/.../140ms 再试,总窗口约 0.5s
        if attempt > 0 {
            std::thread::sleep(std::time::Duration::from_millis(20 * u64::from(attempt)));
        }
        match arboard::Clipboard::new().and_then(|mut c| c.set_text(text)) {
            Ok(()) => return Ok(()),
            Err(e) => last_err = e.to_string(),
        }
    }
    Err(format!("剪贴板写入失败(重试 {MAX_ATTEMPTS} 次仍被占用): {last_err}"))
}

/// 读剪贴板文本；为空或非文本时返回 None
pub fn get_clipboard_text() -> Option<String> {
    let text = arboard::Clipboard::new().ok()?.get_text().ok()?;
    if text.is_empty() {
        None
    } else {
        Some(text)
    }
}

/// 直接从系统剪贴板读取图片（支持截图工具/微信等写入的位图），
/// 编码为 PNG 落盘到临时目录并返回路径；剪贴板里没有图片时返回 None。
/// 不依赖 WebView 的粘贴事件，绕开其对剪贴板图片的暴露限制。
pub fn clipboard_image_to_temp() -> Result<Option<String>, String> {
    let mut clipboard = arboard::Clipboard::new().map_err(|e| e.to_string())?;
    let img = match clipboard.get_image() {
        Ok(i) => i,
        Err(_) => return Ok(None),
    };
    let dir = std::env::temp_dir().join("ccgui");
    fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
    let file = dir.join(format!("paste-{}.png", crate::store::now_ms()));
    let out = fs::File::create(&file).map_err(|e| e.to_string())?;
    let mut encoder = png::Encoder::new(
        std::io::BufWriter::new(out),
        img.width as u32,
        img.height as u32,
    );
    encoder.set_color(png::ColorType::Rgba);
    encoder.set_depth(png::BitDepth::Eight);
    let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
    writer
        .write_image_data(&img.bytes)
        .map_err(|e| e.to_string())?;
    writer.finish().map_err(|e| e.to_string())?;
    Ok(Some(file.to_string_lossy().into_owned()))
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FilePreview {
    /// text | markdown | image | binary | toolarge
    pub kind: String,
    /// 文本内容，或图片的 base64
    pub content: String,
    /// 图片 MIME 类型
    pub mime: String,
    /// 读取时的文件修改时间（保存时做冲突校验）
    pub mtime_ms: u64,
    pub size: u64,
}

const TEXT_LIMIT: u64 = 2 * 1024 * 1024;
const IMAGE_LIMIT: u64 = 20 * 1024 * 1024;

fn image_mime(ext: &str) -> Option<&'static str> {
    match ext.to_ascii_lowercase().as_str() {
        "png" => Some("image/png"),
        "jpg" | "jpeg" => Some("image/jpeg"),
        "gif" => Some("image/gif"),
        "webp" => Some("image/webp"),
        "svg" => Some("image/svg+xml"),
        "bmp" => Some("image/bmp"),
        "ico" => Some("image/x-icon"),
        _ => None,
    }
}

fn mtime_of(path: &Path) -> u64 {
    fs::metadata(path)
        .and_then(|m| m.modified())
        .ok()
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// 读取文件用于预览：按扩展名与内容分流为文本/Markdown/图片/二进制/过大
pub fn read_file_preview(path: &str) -> Result<FilePreview, String> {
    use base64::Engine;
    let p = Path::new(path);
    let meta = fs::metadata(p).map_err(|e| e.to_string())?;
    let size = meta.len();
    let mtime_ms = mtime_of(p);
    let ext = p
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();

    if let Some(mime) = image_mime(&ext) {
        if size > IMAGE_LIMIT {
            return Ok(FilePreview {
                kind: "toolarge".into(),
                content: String::new(),
                mime: String::new(),
                mtime_ms,
                size,
            });
        }
        let bytes = fs::read(p).map_err(|e| e.to_string())?;
        return Ok(FilePreview {
            kind: "image".into(),
            content: base64::engine::general_purpose::STANDARD.encode(bytes),
            mime: mime.into(),
            mtime_ms,
            size,
        });
    }

    if size > TEXT_LIMIT {
        return Ok(FilePreview {
            kind: "toolarge".into(),
            content: String::new(),
            mime: String::new(),
            mtime_ms,
            size,
        });
    }
    let bytes = fs::read(p).map_err(|e| e.to_string())?;
    // 头部出现 NUL 字节视为二进制
    if bytes.iter().take(8192).any(|&b| b == 0) {
        return Ok(FilePreview {
            kind: "binary".into(),
            content: String::new(),
            mime: String::new(),
            mtime_ms,
            size,
        });
    }
    let kind = if ext == "md" || ext == "markdown" {
        "markdown"
    } else {
        "text"
    };
    Ok(FilePreview {
        kind: kind.into(),
        content: String::from_utf8_lossy(&bytes).into_owned(),
        mime: String::new(),
        mtime_ms,
        size,
    })
}

/// 写回文本文件。expected_mtime_ms 非 0 时做冲突校验：
/// 文件在读取后被外部修改过则拒绝，由前端确认后传 0 强制覆盖。
pub fn write_file_text(path: &str, content: &str, expected_mtime_ms: u64) -> Result<u64, String> {
    let p = Path::new(path);
    if expected_mtime_ms != 0 {
        let current = mtime_of(p);
        if current != 0 && current != expected_mtime_ms {
            return Err("conflict".into());
        }
    }
    fs::write(p, content).map_err(|e| e.to_string())?;
    Ok(mtime_of(p))
}

/// 新建文件或文件夹；已存在则报错
pub fn create_entry(path: &str, is_dir: bool) -> Result<(), String> {
    let p = Path::new(path);
    if p.exists() {
        return Err("同名文件或文件夹已存在".into());
    }
    if is_dir {
        fs::create_dir_all(p).map_err(|e| e.to_string())
    } else {
        if let Some(parent) = p.parent() {
            fs::create_dir_all(parent).map_err(|e| e.to_string())?;
        }
        fs::write(p, "").map_err(|e| e.to_string())
    }
}

/// 重命名（同目录内改名）；文件系统根禁止
pub fn rename_entry(path: &str, new_name: &str) -> Result<String, String> {
    reject_root(Path::new(path))?;
    if new_name.trim().is_empty() || new_name.contains(['\\', '/', ':']) {
        return Err("名称不合法".into());
    }
    let p = Path::new(path);
    let parent = p.parent().ok_or("无法获取父目录")?;
    let dest = parent.join(new_name.trim());
    if dest.exists() {
        return Err("目标名称已存在".into());
    }
    fs::rename(p, &dest).map_err(|e| e.to_string())?;
    Ok(dest.to_string_lossy().into_owned())
}

/// 删除：移入回收站（可恢复），不做永久删除；文件系统根禁止
pub fn delete_entry(path: &str) -> Result<(), String> {
    reject_root(Path::new(path))?;
    trash::delete(path).map_err(|e| e.to_string())
}

/// 在资源管理器中显示并选中该文件/文件夹
pub fn reveal_in_explorer(path: &str) -> Result<(), String> {
    std::process::Command::new("explorer.exe")
        .arg("/select,")
        .arg(path)
        .spawn()
        .map(|_| ())
        .map_err(|e| e.to_string())
}

/// 计算相对路径（相对项目根）；不在项目内则返回原路径
pub fn relative_to(base: &str, path: &str) -> String {
    Path::new(path)
        .strip_prefix(base)
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_else(|_| path.to_string())
}

// ---------- 移动端文件管理 ----------

/// 是否文件系统根（C:\ 或 /）：根目录禁止 复制/移动/重命名/删除/打包，防误伤整盘
fn is_fs_root(p: &Path) -> bool {
    p.parent().map(|x| x.as_os_str().is_empty()).unwrap_or(true)
}
fn reject_root(p: &Path) -> Result<(), String> {
    if is_fs_root(p) {
        Err("不允许对磁盘根目录执行此操作".into())
    } else {
        Ok(())
    }
}

/// 编辑用完整文本读取：UTF-8（有损容忍），超过 2MB 拒绝（移动端编辑场景足够）
pub fn read_file_text(path: &str) -> Result<(String, u64), String> {
    const EDIT_LIMIT: u64 = 2 * 1024 * 1024;
    let p = Path::new(path);
    let meta = fs::metadata(p).map_err(|e| e.to_string())?;
    if meta.len() > EDIT_LIMIT {
        return Err(format!("文件超过 2MB，不适合在手机上编辑"));
    }
    let bytes = fs::read(p).map_err(|e| e.to_string())?;
    if bytes.iter().take(8192).any(|&b| b == 0) {
        return Err("二进制文件不能编辑".into());
    }
    Ok((
        String::from_utf8_lossy(&bytes).into_owned(),
        mtime_of(p),
    ))
}

/// 目标目录下的不重名路径：被占时依次尝试「名 (2).ext」「名 (3).ext」…
fn dedup_path(dest_dir: &Path, name: &str) -> PathBuf {
    let candidate = dest_dir.join(name);
    if !candidate.exists() {
        return candidate;
    }
    let (stem, ext) = match name.rfind('.') {
        // 隐藏文件（如 .gitignore）整体当 stem，不拆扩展名
        Some(i) if i > 0 => (&name[..i], &name[i..]),
        _ => (name, ""),
    };
    for n in 2..1000 {
        let c = dest_dir.join(format!("{stem} ({n}){ext}"));
        if !c.exists() {
            return c;
        }
    }
    dest_dir.join(format!("{stem}-{}{ext}", crate::store::now_ms()))
}

/// 递归复制单条目录/文件到 dest_dir（栈遍历，不跟符号链接），返回最终落点路径
fn copy_recursive(src: &Path, dest_dir: &Path) -> Result<PathBuf, String> {
    let name = src
        .file_name()
        .ok_or("无法获取名称")?
        .to_string_lossy()
        .into_owned();
    let target = dedup_path(dest_dir, &name);
    let ft = fs::symlink_metadata(src)
        .map_err(|e| e.to_string())?
        .file_type();
    if ft.is_symlink() {
        return Err("不支持复制符号链接".into());
    }
    if ft.is_file() {
        fs::copy(src, &target).map_err(|e| e.to_string())?;
        return Ok(target);
    }
    // 目录：栈式遍历（源子路径 → 目标子路径），逐层建目录、逐文件复制
    fs::create_dir_all(&target).map_err(|e| e.to_string())?;
    let mut stack: Vec<(PathBuf, PathBuf)> = vec![(src.to_path_buf(), target.clone())];
    while let Some((sdir, tdir)) = stack.pop() {
        for entry in fs::read_dir(&sdir).map_err(|e| e.to_string())?.flatten() {
            let sp = entry.path();
            let tp = tdir.join(entry.file_name());
            let eft = entry.file_type().map_err(|e| e.to_string())?;
            if eft.is_symlink() {
                continue; // 跳过符号链接，防循环
            }
            if eft.is_dir() {
                fs::create_dir_all(&tp).map_err(|e| e.to_string())?;
                stack.push((sp, tp));
            } else {
                fs::copy(&sp, &tp).map_err(|e| e.to_string())?;
            }
        }
    }
    Ok(target)
}

/// 复制文件/目录到 dest_dir；重名自动加序号，返回最终路径
pub fn copy_entry(src: &str, dest_dir: &str) -> Result<String, String> {
    let s = Path::new(src);
    let d = Path::new(dest_dir);
    reject_root(s)?;
    if !s.exists() {
        return Err(format!("源不存在: {src}"));
    }
    if !d.is_dir() {
        return Err(format!("目标目录不存在: {dest_dir}"));
    }
    // 禁止把目录复制进自己内部
    if d.starts_with(s) {
        return Err("不能复制到自身内部".into());
    }
    Ok(copy_recursive(s, d)?.to_string_lossy().into_owned())
}

/// 移动文件/目录到 dest_dir：同卷 rename 秒移；跨卷 = 复制 + 源进回收站（可恢复）
pub fn move_entry(src: &str, dest_dir: &str) -> Result<String, String> {
    let s = Path::new(src);
    let d = Path::new(dest_dir);
    reject_root(s)?;
    if !s.exists() {
        return Err(format!("源不存在: {src}"));
    }
    if !d.is_dir() {
        return Err(format!("目标目录不存在: {dest_dir}"));
    }
    if d.starts_with(s) {
        return Err("不能移动到自身内部".into());
    }
    let name = s
        .file_name()
        .ok_or("无法获取名称")?
        .to_string_lossy()
        .into_owned();
    // 同卷快路径：目标位置不重名时直接 rename
    let direct = d.join(&name);
    if !direct.exists() && fs::rename(s, &direct).is_ok() {
        return Ok(direct.to_string_lossy().into_owned());
    }
    // 跨卷或重名：复制（自动加序号）后源进回收站
    let landed = copy_recursive(s, d)?;
    trash::delete(s).map_err(|e| format!("已复制到目标，但源删除失败: {e}"))?;
    Ok(landed.to_string_lossy().into_owned())
}

/// 把若干文件/目录打包成 zip（deflate），dest_zip 重名自动加序号，返回最终 zip 路径。
/// 包内路径：单条目以其名为根；目录递归展开，跳过符号链接。
pub fn zip_entries(paths: &[String], dest_zip: &str) -> Result<String, String> {
    use std::io::Write;
    // 先全量校验再建 zip，避免失败后留下空包
    for src in paths {
        let s = Path::new(src);
        reject_root(s)?;
        if !s.exists() {
            return Err(format!("源不存在: {src}"));
        }
    }
    let d = Path::new(dest_zip);
    let parent = d.parent().ok_or("目标路径无父目录")?;
    if !parent.is_dir() {
        return Err(format!("目标目录不存在: {}", parent.display()));
    }
    let fname = d
        .file_name()
        .ok_or("目标不是文件路径")?
        .to_string_lossy()
        .into_owned();
    let final_path = dedup_path(parent, &fname);
    let file = fs::File::create(&final_path).map_err(|e| e.to_string())?;
    let mut zw = zip::ZipWriter::new(std::io::BufWriter::new(file));
    let opts = zip::write::SimpleFileOptions::default()
        .compression_method(zip::CompressionMethod::Deflated);
    let mut buf = Vec::new();
    for src in paths {
        let s = Path::new(src);
        let base = s
            .file_name()
            .ok_or("无法获取名称")?
            .to_string_lossy()
            .into_owned();
        // 栈：（源路径, 包内前缀）
        let mut stack: Vec<(PathBuf, String)> = vec![(s.to_path_buf(), base)];
        while let Some((sp, prefix)) = stack.pop() {
            let ft = fs::symlink_metadata(&sp)
                .map_err(|e| e.to_string())?
                .file_type();
            if ft.is_symlink() {
                continue;
            }
            if ft.is_dir() {
                zw.add_directory(format!("{prefix}/"), opts)
                    .map_err(|e| e.to_string())?;
                for entry in fs::read_dir(&sp).map_err(|e| e.to_string())?.flatten() {
                    let child_prefix = format!("{prefix}/{}", entry.file_name().to_string_lossy());
                    stack.push((entry.path(), child_prefix));
                }
            } else {
                zw.start_file(prefix, opts).map_err(|e| e.to_string())?;
                buf.clear();
                std::io::Read::read_to_end(&mut fs::File::open(&sp).map_err(|e| e.to_string())?, &mut buf)
                    .map_err(|e| e.to_string())?;
                zw.write_all(&buf).map_err(|e| e.to_string())?;
            }
        }
    }
    zw.finish().map_err(|e| e.to_string())?;
    Ok(final_path.to_string_lossy().into_owned())
}

/// 解压 zip 到 dest_dir（zip-slip 防护：拒绝跳出目标目录的条目），返回解压到的目录
pub fn unzip_to(zip_path: &str, dest_dir: &str) -> Result<String, String> {
    let z = Path::new(zip_path);
    if !z.is_file() {
        return Err(format!("zip 不存在: {zip_path}"));
    }
    let d = Path::new(dest_dir);
    if !d.is_dir() {
        return Err(format!("目标目录不存在: {dest_dir}"));
    }
    let file = fs::File::open(z).map_err(|e| e.to_string())?;
    let mut archive = zip::ZipArchive::new(std::io::BufReader::new(file)).map_err(|e| e.to_string())?;
    for i in 0..archive.len() {
        let mut entry = archive.by_index(i).map_err(|e| e.to_string())?;
        // enclosed_name 拒绝 ../ 与绝对路径，防 zip-slip
        let Some(rel) = entry.enclosed_name() else {
            continue;
        };
        let out = d.join(rel);
        if entry.is_dir() {
            fs::create_dir_all(&out).map_err(|e| e.to_string())?;
        } else {
            if let Some(p) = out.parent() {
                fs::create_dir_all(p).map_err(|e| e.to_string())?;
            }
            let mut of = fs::File::create(&out).map_err(|e| e.to_string())?;
            std::io::copy(&mut entry, &mut of).map_err(|e| e.to_string())?;
        }
    }
    Ok(d.to_string_lossy().into_owned())
}
