use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, Seek, Write};
use std::path::{Path, PathBuf};

use zip::write::SimpleFileOptions;

use crate::AppResult;
use crate::temp::{TempFileGuard, temp_path};

pub(crate) fn create_zip(zip_args: &[String]) -> AppResult<TempFileGuard> {
    let (temp_zip, inputs) = zip_request(zip_args)?;

    let file = File::create(&temp_zip)?;
    let mut writer = zip::ZipWriter::new(file);
    let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);

    for input in inputs {
        let path = Path::new(input);
        if !path.exists() {
            return Err(io::Error::other(format!("Path does not exist: {input}")).into());
        }
        add_path_to_zip(&mut writer, path, path, options)?;
    }

    writer.finish()?;
    Ok(TempFileGuard::new(temp_zip))
}

fn zip_request(zip_args: &[String]) -> AppResult<(PathBuf, Vec<&String>)> {
    let args = zip_args
        .iter()
        .filter(|input| input.as_str() != "-r" && input.as_str() != "--recursive")
        .collect::<Vec<_>>();

    if args.is_empty() {
        return Err(io::Error::other("--zip requires files/directories to zip").into());
    }

    if args.len() >= 2 && looks_like_zip_name(args[0]) {
        return Ok((std::env::temp_dir().join(args[0]), args[1..].to_vec()));
    }

    Ok((temp_path("tgn-archive", "zip")?, args))
}

fn looks_like_zip_name(value: &str) -> bool {
    Path::new(value)
        .extension()
        .and_then(OsStr::to_str)
        .is_some_and(|extension| extension.eq_ignore_ascii_case("zip"))
}

fn add_path_to_zip<W: Write + Seek>(
    writer: &mut zip::ZipWriter<W>,
    path: &Path,
    base: &Path,
    options: SimpleFileOptions,
) -> AppResult<()> {
    if path.is_file() {
        let name = zip_entry_name(path, base)?;
        writer.start_file(name, options)?;
        let mut file = File::open(path)?;
        io::copy(&mut file, writer)?;
        return Ok(());
    }

    if path.is_dir() {
        for entry in fs::read_dir(path)? {
            let entry = entry?;
            add_path_to_zip(writer, &entry.path(), base, options)?;
        }
    }

    Ok(())
}

fn zip_entry_name(path: &Path, base: &Path) -> AppResult<String> {
    let name = if base.is_file() {
        path.file_name()
            .and_then(OsStr::to_str)
            .ok_or_else(|| io::Error::other("Invalid file name"))?
            .to_string()
    } else {
        path.strip_prefix(base.parent().unwrap_or_else(|| Path::new("")))?
            .to_string_lossy()
            .to_string()
    };

    Ok(name.replace('\\', "/"))
}

#[cfg(test)]
mod tests {
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::*;
    use crate::temp::{TempFileGuard, temp_path};

    #[test]
    fn zip_entry_preserves_top_level_directory() {
        let path = Path::new("src/main.rs");
        let name = zip_entry_name(path, Path::new("src")).unwrap();
        assert_eq!(name, "src/main.rs");
    }

    #[test]
    fn creates_zip_from_file_input() {
        let source = temp_path("tgn-test-source", "txt").unwrap();
        fs::write(&source, "hello").unwrap();
        let source_guard = TempFileGuard::new(source.clone());
        let zip = create_zip(&[source_guard.path().to_string_lossy().to_string()]).unwrap();

        let file = File::open(zip.path()).unwrap();
        let mut archive = zip::ZipArchive::new(file).unwrap();
        assert_eq!(archive.len(), 1);
        assert_eq!(
            archive.by_index(0).unwrap().name(),
            source_guard.path().file_name().unwrap().to_str().unwrap()
        );
    }

    #[test]
    fn creates_zip_from_directory_input_recursively() {
        let directory = temp_path("tgn-test-directory", "dir").unwrap();
        fs::create_dir(&directory).unwrap();
        let nested = directory.join("nested.txt");
        fs::write(&nested, "hello").unwrap();

        let zip = create_zip(&[directory.to_string_lossy().to_string()]).unwrap();

        let file = File::open(zip.path()).unwrap();
        let mut archive = zip::ZipArchive::new(file).unwrap();
        assert_eq!(archive.len(), 1);
        assert_eq!(
            archive.by_index(0).unwrap().name(),
            format!(
                "{}/nested.txt",
                directory.file_name().unwrap().to_str().unwrap()
            )
        );
        fs::remove_dir_all(&directory).unwrap();
    }

    #[test]
    fn accepts_explicit_zip_name_for_compatibility() {
        let source = temp_path("tgn-test-named-source", "txt").unwrap();
        fs::write(&source, "hello").unwrap();
        let source_guard = TempFileGuard::new(source.clone());
        let zip_name = format!(
            "tgn-test-{}.zip",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        );

        let zip =
            create_zip(&[zip_name, source_guard.path().to_string_lossy().to_string()]).unwrap();

        assert_eq!(zip.path().extension().unwrap(), "zip");
    }

    #[test]
    fn zip_mode_accepts_recursive_flag_for_bash_compatibility() {
        let source = temp_path("tgn-test-recursive-source", "txt").unwrap();
        fs::write(&source, "hello").unwrap();
        let source_guard = TempFileGuard::new(source.clone());

        let zip = create_zip(&[
            "-r".to_string(),
            source_guard.path().to_string_lossy().to_string(),
        ])
        .unwrap();

        let file = File::open(zip.path()).unwrap();
        let archive = zip::ZipArchive::new(file).unwrap();
        assert_eq!(archive.len(), 1);
    }
}
