use std::path::{Path, PathBuf};

use anyhow::{anyhow, bail};
use gix::{NestedProgress, Progress, worktree::archive};

pub struct Options {
    pub format: Option<archive::Format>,
    pub files: Vec<(String, String)>,
    pub prefix: Option<String>,
    pub add_paths: Vec<PathBuf>,
}

pub fn stream(
    repo: gix::Repository,
    destination_path: &Path,
    rev_spec: Option<&str>,
    mut progress: impl NestedProgress,
    Options {
        format,
        prefix,
        add_paths,
        files,
    }: Options,
) -> anyhow::Result<()> {
    let format = format.map_or_else(|| format_from_ext(destination_path), Ok)?;
    let object = repo.rev_parse_single(rev_spec.unwrap_or("HEAD"))?.object()?;
    let (modification_date, tree) = fetch_rev_info(object)?;

    let start = std::time::Instant::now();
    let (mut stream, index) = repo.worktree_stream(tree)?;
    if !add_paths.is_empty() {
        let root = gix::path::realpath(
            repo.workdir()
                .ok_or_else(|| anyhow!("Adding files requires a worktree directory that contains them"))?,
        )?;
        for path in add_paths {
            stream.add_entry_from_path(&root, &gix::path::realpath(&path)?, repo.object_hash())?;
        }
    }
    for (path, content) in files {
        stream.add_entry(gix::worktree::stream::AdditionalEntry {
            id: gix::hash::Kind::Sha1.null(),
            mode: gix::object::tree::EntryKind::Blob.into(),
            relative_path: path.into(),
            source: gix::worktree::stream::entry::Source::Memory(content.into()),
        });
    }

    let mut entries = progress.add_child("entries");
    entries.init(Some(index.entries().len()), gix::progress::count("entries"));
    let mut bytes = progress.add_child("written");
    bytes.init(None, gix::progress::bytes());

    let mut file = gix::progress::Write {
        inner: std::io::BufWriter::with_capacity(128 * 1024, std::fs::File::create(destination_path)?),
        progress: &mut bytes,
    };
    repo.worktree_archive(
        stream,
        &mut file,
        &mut entries,
        &gix::interrupt::IS_INTERRUPTED,
        gix::worktree::archive::Options {
            format,
            tree_prefix: prefix.map(gix::bstr::BString::from),
            modification_time: modification_date.unwrap_or_else(|| {
                std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .map(|d| d.as_secs())
                    .unwrap_or_default() as gix::date::SecondsSinceUnixEpoch
            }),
        },
    )?;

    entries.show_throughput(start);
    bytes.show_throughput(start);

    Ok(())
}

fn fetch_rev_info(
    object: gix::Object<'_>,
) -> anyhow::Result<(Option<gix::date::SecondsSinceUnixEpoch>, gix::ObjectId)> {
    Ok(match object.kind {
        gix::object::Kind::Commit => {
            let commit = object.into_commit();
            (Some(commit.committer()?.seconds()), commit.tree_id()?.detach())
        }
        gix::object::Kind::Tree => (None, object.id),
        gix::object::Kind::Tag => fetch_rev_info(object.peel_to_kind(gix::object::Kind::Commit)?)?,
        gix::object::Kind::Blob => bail!("Cannot derive commit or tree from blob at {}", object.id),
    })
}

fn format_from_ext(path: &Path) -> anyhow::Result<archive::Format> {
    Ok(match path.extension().and_then(std::ffi::OsStr::to_str) {
        None => bail!("Cannot derive archive format from a file without extension"),
        Some("tar") => archive::Format::Tar,
        Some("gz") => archive::Format::TarGz {
            compression_level: None,
        },
        Some("zip") => archive::Format::Zip {
            compression_level: None,
        },
        Some("stream") => archive::Format::InternalTransientNonPersistable,
        Some(ext) => bail!("Format for extension '{ext}' is unsupported"),
    })
}
