/* archive.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::fs::File;
use std::io::Write;
use std::path::Path;

use git2::TreeWalkMode;

use super::{git_err, open_repo};

enum ArchiveFormat {
    TarGz,
    TarXz,
    Zip,
}

impl ArchiveFormat {
    fn from_path(path: &Path) -> Option<Self> {
        let name = path.file_name()?.to_str()?;

        if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
            Some(Self::TarGz)
        } else if name.ends_with(".tar.xz") || name.ends_with(".txz") {
            Some(Self::TarXz)
        } else if name.ends_with(".zip") {
            Some(Self::Zip)
        } else {
            None
        }
    }
}

struct TreeEntry {
    path: String,
    data: Vec<u8>,
    mode: u32,
}

fn collect_tree_entries(tree: &git2::Tree, repo: &git2::Repository) -> Result<Vec<TreeEntry>, String> {
    let mut entries = Vec::new();
    let entries_ref = &mut entries;

    tree.walk(TreeWalkMode::PreOrder, |root, entry| {
        if entry.kind() == Some(git2::ObjectType::Blob) {
            let path = if root.is_empty() {
                entry.name().unwrap_or("").to_string()
            } else {
                format!("{}{}", root, entry.name().unwrap_or(""))
            };

            if let Ok(blob) = repo.find_blob(entry.id()) {
                entries_ref.push(TreeEntry {
                    path,
                    data: blob.content().to_vec(),
                    mode: entry.filemode() as u32,
                });
            }
        }

        git2::TreeWalkResult::Ok
    })
    .map_err(git_err("Failed to walk tree"))?;

    Ok(entries)
}

fn write_tar_gz(entries: &[TreeEntry], output_path: &Path) -> Result<(), String> {
    let file = File::create(output_path).map_err(|e| format!("Failed to create file: {e}"))?;
    let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
    let mut archive = tar::Builder::new(encoder);

    for entry in entries {
        let mut header = tar::Header::new_gnu();
        header.set_size(entry.data.len() as u64);
        header.set_mode(entry.mode);
        header.set_cksum();

        archive
            .append_data(&mut header, &entry.path, entry.data.as_slice())
            .map_err(|e| format!("Failed to write archive entry: {e}"))?;
    }

    archive
        .finish()
        .map_err(|e| format!("Failed to finalize archive: {e}"))?;

    Ok(())
}

fn write_tar_xz(entries: &[TreeEntry], output_path: &Path) -> Result<(), String> {
    let file = File::create(output_path).map_err(|e| format!("Failed to create file: {e}"))?;
    let encoder = xz2::write::XzEncoder::new(file, 6);
    let mut archive = tar::Builder::new(encoder);

    for entry in entries {
        let mut header = tar::Header::new_gnu();
        header.set_size(entry.data.len() as u64);
        header.set_mode(entry.mode);
        header.set_cksum();

        archive
            .append_data(&mut header, &entry.path, entry.data.as_slice())
            .map_err(|e| format!("Failed to write archive entry: {e}"))?;
    }

    archive
        .finish()
        .map_err(|e| format!("Failed to finalize archive: {e}"))?;

    Ok(())
}

fn write_zip(entries: &[TreeEntry], output_path: &Path) -> Result<(), String> {
    let file = File::create(output_path).map_err(|e| format!("Failed to create file: {e}"))?;
    let mut zip = zip::ZipWriter::new(file);

    for entry in entries {
        let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);

        zip.start_file(&entry.path, options)
            .map_err(|e| format!("Failed to start zip entry: {e}"))?;

        zip.write_all(&entry.data)
            .map_err(|e| format!("Failed to write zip entry: {e}"))?;
    }

    zip.finish().map_err(|e| format!("Failed to finalize zip: {e}"))?;

    Ok(())
}

pub fn create_archive(repo_path: &Path, rev_spec: &str, output_path: &Path) -> Result<(), String> {
    let format = ArchiveFormat::from_path(output_path)
        .ok_or_else(|| String::from("Unsupported archive format. Use .tar.gz, .tar.xz, or .zip"))?;

    let repo = open_repo(repo_path)?;

    let obj = repo
        .revparse_single(rev_spec)
        .map_err(git_err("Failed to resolve revision"))?;

    let tree = obj.peel_to_tree().map_err(git_err("Failed to resolve tree"))?;
    let entries = collect_tree_entries(&tree, &repo)?;

    match format {
        ArchiveFormat::TarGz => write_tar_gz(&entries, output_path),
        ArchiveFormat::TarXz => write_tar_xz(&entries, output_path),
        ArchiveFormat::Zip => write_zip(&entries, output_path),
    }
}
