use std::{
    ffi::OsStr,
    fs,
    io::{stderr, Write},
    path::Path,
    process::{Command, Stdio},
};

use indicatif::ProgressBar;
use walkdir::WalkDir;

use crate::error::Error;

pub fn run_external_command<S0: AsRef<OsStr>, S1: AsRef<OsStr>, I: IntoIterator<Item = S1>>(
    cmd: S0,
    args: I,
    mute: bool,
) -> Result<(), Error> {
    let mut command = Command::new(cmd);
    command.args(args);
    if mute {
        command.stderr(Stdio::null()).stdout(Stdio::null());
    }
    log::trace!("Run external: {:?}", command);
    let status = command.status()?;
    if status.success() {
        Ok(())
    } else {
        Err(Error::ExternalCommandFailed)
    }
}

pub fn is_newer(src: &Path, dst: &Path) -> bool {
    let (Ok(src), Ok(dst)) = (
        src.metadata().and_then(|m| m.modified()),
        dst.metadata().and_then(|m| m.modified()),
    ) else {
        return true;
    };

    src > dst
}

pub fn copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> {
    let src = src.as_ref();
    let dst = dst.as_ref();
    fs::copy(src, dst)
        .map_err(|e| Error::CopyFileError(e, src.to_owned(), dst.to_owned()))
        .map(|_| ())
}

pub fn copy_file_if<S: AsRef<Path>, D: AsRef<Path>, P: Fn(&Path, &Path) -> bool>(
    src: S,
    dst: D,
    predicate: P,
) -> Result<(), Error> {
    let src = src.as_ref();
    let dst = dst.as_ref();
    if !dst.exists() || predicate(src, dst) {
        log::debug!("Copy {} -> {}", src.display(), dst.display());
        copy_file(src, dst)?;
    }
    Ok(())
}

pub fn copy_dir_recursive_if<S: AsRef<Path>, D: AsRef<Path>, P: Fn(&Path, &Path) -> bool>(
    src: S,
    dst: D,
    predicate: P,
) -> Result<(), Error> {
    let src = src.as_ref();
    let dst = dst.as_ref();

    for entry in WalkDir::new(src).follow_links(false) {
        let entry = entry?;
        let src_path = entry.path();
        let rel_path = src_path
            .strip_prefix(src)
            .map_err(|_| Error::InvalidPath(src_path.into()))?;
        let dst_path = dst.join(rel_path);

        if entry.file_type().is_dir() {
            fs::create_dir_all(dst_path)?;
        } else {
            copy_file_if(src_path, dst_path, &predicate)?;
        }
    }

    Ok(())
}

pub fn copy_dir_recursive<S: AsRef<Path>, D: AsRef<Path>>(src: S, dst: D) -> Result<(), Error> {
    copy_dir_recursive_if(src, dst, |_, _| true)
}

pub fn git_clone(
    dst: impl AsRef<Path>,
    url: &str,
    branch: &str,
    shallow: bool,
) -> Result<(), Error> {
    let path = dst.as_ref();
    match git2::Repository::open(path) {
        Ok(_) => {}
        Err(err) if err.code() == git2::ErrorCode::NotFound => {
            log::info!("Fetching: src={}, branch={}", url, branch);
            let progress_bar = ProgressBar::no_length();

            let mut remote_cbs = git2::RemoteCallbacks::new();
            remote_cbs.sideband_progress(|bytes| {
                stderr().write_all(bytes).ok();
                true
            });
            remote_cbs.transfer_progress(|progress| {
                progress_bar.set_message("Receiving objects");
                if progress_bar.length().is_none() {
                    progress_bar.set_length(progress.total_objects() as _);
                }
                progress_bar.set_position(progress.received_objects() as _);
                true
            });

            let mut fetch_opts = git2::FetchOptions::new();
            if shallow {
                fetch_opts.depth(1);
            }
            fetch_opts.remote_callbacks(remote_cbs);

            git2::build::RepoBuilder::new()
                .branch(branch)
                .fetch_options(fetch_opts)
                .clone(url, &path)?;

            progress_bar.finish_and_clear();
        }
        Err(err) => return Err(err.into()),
    }

    Ok(())
}
