use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;

const CACHE_DIR: &str = "data/cache";

// Phase 1: built-in macOS tools (no installation)
const TEXTUTIL_FORMATS: &[&str] = &["docx", "doc", "rtf"];
const APPLE_FORMATS: &[&str] = &["pages", "key", "numbers"];

// Phase 2: soffice/qlmanage fallback
const SOFFICE_FORMATS: &[&str] = &["pptx", "ppt", "xlsx", "xls", "odt", "epub"];

pub fn is_document_ext(ext: &str) -> bool {
    TEXTUTIL_FORMATS.contains(&ext)
        || APPLE_FORMATS.contains(&ext)
        || SOFFICE_FORMATS.contains(&ext)
}

pub fn is_textutil_ext(ext: &str) -> bool {
    TEXTUTIL_FORMATS.contains(&ext)
}

pub fn is_apple_format_ext(ext: &str) -> bool {
    APPLE_FORMATS.contains(&ext)
}

pub fn get_cached_preview(file_uuid: &str, ext: &str) -> Option<(PathBuf, &'static str)> {
    if TEXTUTIL_FORMATS.contains(&ext) {
        get_cached_txt(file_uuid).map(|p| (p, "text/plain; charset=utf-8"))
    } else if APPLE_FORMATS.contains(&ext) {
        get_cached_jpg(file_uuid).map(|p| (p, "image/jpeg"))
    } else {
        get_cached_pdf(file_uuid).map(|p| (p, "application/pdf"))
    }
}

pub fn get_cached_txt(file_uuid: &str) -> Option<PathBuf> {
    let p = Path::new(CACHE_DIR).join(format!("{}.txt", file_uuid));
    p.exists().then_some(p)
}

pub fn get_cached_jpg(file_uuid: &str) -> Option<PathBuf> {
    let p = Path::new(CACHE_DIR).join(format!("{}.jpg", file_uuid));
    p.exists().then_some(p)
}

pub fn get_cached_pdf(file_uuid: &str) -> Option<PathBuf> {
    let p = Path::new(CACHE_DIR).join(format!("{}.pdf", file_uuid));
    p.exists().then_some(p)
}

pub fn get_cached_png(file_uuid: &str) -> Option<PathBuf> {
    let p = Path::new(CACHE_DIR).join(format!("{}.png", file_uuid));
    p.exists().then_some(p)
}

pub fn convert_document(input_path: &Path, file_uuid: &str) -> Result<(PathBuf, &'static str)> {
    let ext = input_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_lowercase();

    // Phase 1: built-in tools (fast, no installation)
    if TEXTUTIL_FORMATS.contains(&ext.as_str()) {
        let p = textutil_to_txt(input_path, file_uuid)?;
        return Ok((p, "text/plain; charset=utf-8"));
    }

    if APPLE_FORMATS.contains(&ext.as_str()) {
        match unzip_preview_jpg(input_path, file_uuid) {
            Ok(p) => return Ok((p, "image/jpeg")),
            Err(e) => eprintln!("[markbase] unzip preview failed for {}: {}", file_uuid, e),
        }
        // Fall back to qlmanage PNG
        let p = qlmanage_to_png(input_path, file_uuid, 2048)?;
        return Ok((p, "image/png"));
    }

    // Phase 2: soffice for Office formats
    if SOFFICE_FORMATS.contains(&ext.as_str()) {
        match soffice_to_pdf(input_path, file_uuid) {
            Ok(p) => return Ok((p, "application/pdf")),
            Err(e) => {
                eprintln!("[markbase] soffice failed for {}: {}", file_uuid, e);
            }
        }
    }

    // Final fallback: qlmanage PNG
    let p = qlmanage_to_png(input_path, file_uuid, 2048)?;
    Ok((p, "image/png"))
}

// ─── Phase 1: textutil (macOS built-in, .docx/.doc/.rtf → .txt) ───

fn textutil_to_txt(input_path: &Path, file_uuid: &str) -> Result<PathBuf> {
    let cache_dir = Path::new(CACHE_DIR);
    std::fs::create_dir_all(cache_dir)?;

    let output = cache_dir.join(format!("{}.txt", file_uuid));
    if output.exists() {
        return Ok(output);
    }

    let out = Command::new("textutil")
        .args(["-convert", "txt", "-output"])
        .arg(&output)
        .arg(input_path)
        .output()
        .context("Failed to run textutil")?;

    if !out.status.success() {
        anyhow::bail!("textutil: {}", String::from_utf8_lossy(&out.stderr).trim());
    }

    if output.exists() {
        Ok(output)
    } else {
        anyhow::bail!("textutil did not produce {}", output.display())
    }
}

// ─── Phase 1: unzip preview.jpg from iWork packages ───

fn unzip_preview_jpg(input_path: &Path, file_uuid: &str) -> Result<PathBuf> {
    let cache_dir = Path::new(CACHE_DIR);
    std::fs::create_dir_all(cache_dir)?;

    let output = cache_dir.join(format!("{}.jpg", file_uuid));
    if output.exists() {
        return Ok(output);
    }

    let tmp = cache_dir.join(format!("_tmp_{}", file_uuid));
    let _ = std::fs::remove_dir_all(&tmp);

    let out = Command::new("unzip")
        .args(["-o", "-d"])
        .arg(&tmp)
        .arg(input_path)
        .output()
        .context("Failed to unzip iWork package")?;

    if !out.status.success() {
        anyhow::bail!("unzip: {}", String::from_utf8_lossy(&out.stderr).trim());
    }

    // Look for preview.jpg, preview.pdf, or quicklook/thumbnail.jpg
    for name in &[
        "preview.jpg",
        "preview.png",
        "preview.pdf",
        "preview-web.jpg",
    ] {
        let src = tmp.join(name);
        if src.exists() {
            std::fs::copy(&src, &output)?;
            let _ = std::fs::remove_dir_all(&tmp);
            return Ok(output);
        }
    }

    let _ = std::fs::remove_dir_all(&tmp);
    anyhow::bail!("no preview found in iWork package")
}

// ─── Phase 2: soffice (LibreOffice, multi-page PDF) ───

fn soffice_to_pdf(input_path: &Path, file_uuid: &str) -> Result<PathBuf> {
    let cache_dir = Path::new(CACHE_DIR);
    std::fs::create_dir_all(cache_dir)?;

    let output = cache_dir.join(format!("{}.pdf", file_uuid));
    if output.exists() {
        return Ok(output);
    }

    let out = Command::new("soffice")
        .args(["--headless", "--convert-to", "pdf", "--outdir"])
        .arg(cache_dir)
        .arg(input_path)
        .output()
        .context("Failed to run soffice")?;

    if !out.status.success() {
        anyhow::bail!("soffice: {}", String::from_utf8_lossy(&out.stderr).trim());
    }

    let basename = input_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown");
    let generated = cache_dir.join(format!("{}.pdf", basename));
    if generated.exists() && generated != output {
        std::fs::rename(&generated, &output)?;
    }

    if output.exists() {
        Ok(output)
    } else {
        anyhow::bail!("soffice did not produce {}", output.display())
    }
}

// ─── Phase 2: qlmanage (Apple QuickLook, PNG thumbnail) ───

fn qlmanage_to_png(input_path: &Path, file_uuid: &str, size: u32) -> Result<PathBuf> {
    let cache_dir = Path::new(CACHE_DIR);
    std::fs::create_dir_all(cache_dir)?;

    let output = cache_dir.join(format!("{}.png", file_uuid));
    if output.exists() {
        return Ok(output);
    }

    let out = Command::new("qlmanage")
        .args(["-t", "-s", &size.to_string(), "-o"])
        .arg(cache_dir)
        .arg(input_path)
        .output()
        .context("Failed to run qlmanage")?;

    if !out.status.success() {
        anyhow::bail!("qlmanage: {}", String::from_utf8_lossy(&out.stderr).trim());
    }

    let filename = input_path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown");
    let generated = cache_dir.join(format!("{}.png", filename));
    if generated.exists() && generated != output {
        std::fs::rename(&generated, &output)?;
    }

    if output.exists() {
        Ok(output)
    } else {
        anyhow::bail!("qlmanage did not produce {}", output.display())
    }
}
