// Copyright (c) 2021 ETH Zurich
// Michael Rogenmoser <michaero@iis.ee.ethz.ch>

//! The `snapshot` subcommand.

use std::path::PathBuf;
use std::process::Command as SysCommand;

use clap::Args;
use indexmap::IndexMap;
use miette::{Context as _, IntoDiagnostic as _};
use tokio::runtime::Runtime;

use crate::Result;
use crate::bail;
use crate::cli::{remove_symlink_dir, symlink_dir};
use crate::cmd::clone::get_path_subdeps;
use crate::config::{Dependency, Locked, LockedSource};
use crate::diagnostic::Warnings;
use crate::infoln;
use crate::sess::{DependencySource, Session, SessionIo};
use crate::{fmt_path, fmt_pkg, stageln};

/// Snapshot the cloned IPs from the working directory into the Bender.lock file
#[derive(Args, Debug)]
pub struct SnapshotArgs {
    /// Working directory to snapshot dependencies from
    #[arg(long, default_value = "working_dir")]
    pub working_dir: String,

    /// Do not skip dependencies that are dirty
    #[arg(long)]
    pub no_skip: bool,

    /// Checkout the dependencies snapshotted into the lockfile
    #[arg(short, long)]
    pub checkout: bool,

    /// Force update of dependencies in a custom checkout_dir. Please use carefully to avoid losing work.
    #[arg(long, requires = "checkout")]
    pub force: bool,
}

/// Execute the `snapshot` subcommand.
pub fn run(sess: &Session, args: &SnapshotArgs) -> Result<()> {
    let mut snapshot_list = Vec::new();

    // Loop through existing deps to find the ones that are overridden to the working directory
    for (name, dep) in sess.config.overrides.iter() {
        if let Dependency::Path {
            path: override_path,
            ..
        } = dep
            && override_path.starts_with(sess.root.join(&args.working_dir))
            && let DependencySource::Path(dep_path) =
                sess.dependency_source(sess.dependency_with_name(name)?)
            && dep_path == *override_path
        {
            // check state, skip & warn if dirty
            if !SysCommand::new(&sess.config.git)
                .arg("status")
                .arg("--porcelain")
                .current_dir(&dep_path)
                .output()
                .into_diagnostic()?
                .stdout
                .is_empty()
                && !args.no_skip
            {
                Warnings::SkippingDirtyDep { pkg: name.clone() }.emit();
                continue;
            }

            // Get the git url and hash of the dependency
            let url = match String::from_utf8(
                SysCommand::new(&sess.config.git)
                    .arg("remote")
                    .arg("get-url")
                    .arg("origin")
                    .current_dir(&dep_path)
                    .output()
                    .into_diagnostic()?
                    .stdout,
            ) {
                Ok(url) => url.trim_end_matches(&['\r', '\n'][..]).to_string(),
                Err(_) => bail!("Failed to get git url."),
            };
            let hash = match String::from_utf8(
                SysCommand::new(&sess.config.git)
                    .arg("rev-parse")
                    .arg("HEAD")
                    .current_dir(&dep_path)
                    .output()
                    .into_diagnostic()?
                    .stdout,
            ) {
                Ok(hash) => hash.trim_end_matches(&['\r', '\n'][..]).to_string(),
                Err(_) => bail!("Failed to get git hash."),
            };

            eprintln!("Snapshotting {} at {} from {}", name, hash, url);

            snapshot_list.push((name.clone(), url, hash));
        }
    }

    // Update the Bender.local to keep changes
    let local_path = sess.root.join("Bender.local");
    if local_path.exists() && !snapshot_list.is_empty() {
        let local_file_str = std::fs::read_to_string(&local_path)
            .into_diagnostic()
            .wrap_err("Reading Bender.local failed")?;
        let mut new_str = String::new();
        if local_file_str.contains("overrides:") {
            let split = local_file_str.split('\n');
            let test = split.clone().next_back().unwrap().is_empty();
            for i in split {
                for (name, _, _) in &snapshot_list {
                    if i.contains(name) {
                        new_str.push('#');
                    }
                }
                new_str.push_str(i);
                new_str.push('\n');
                if i.contains("overrides:") {
                    for (name, url, hash) in &snapshot_list {
                        let dep_str = format!(
                            "  {}: {{ git: \"{}\", rev: \"{}\" }} # Temporary override by Bender using `bender snapshot` command\n",
                            name, url, hash
                        );
                        new_str.push_str(&dep_str);
                    }
                }
            }
            if test {
                // Ensure trailing newline is not duplicated
                new_str.pop();
            }
            std::fs::write(local_path.clone(), new_str)
                .into_diagnostic()
                .wrap_err("Writing new Bender.local failed")?;
            infoln!("Bender.local updated with snapshots.");
        }
    }

    let rt = Runtime::new().into_diagnostic()?;
    let io = SessionIo::new(sess);
    let mut path_subdeps: IndexMap<String, PathBuf> = IndexMap::new();

    for (name, url, _) in &snapshot_list {
        // let old_path = sess.get_package_path(depref);
        // let new_path = sess.get_depsource_path(name, &DependencySource::Git(url.clone()));
        get_path_subdeps(
            &io,
            &rt,
            &sess.get_depsource_path(name, &DependencySource::Git(url.clone())),
            sess.dependency_with_name(name)?,
        )?
        .into_iter()
        .for_each(|(k, v)| {
            path_subdeps.insert(k, v);
        });
    }

    // Update the Bender.lock file with the new hash
    use std::fs::File;
    let file = File::open(sess.root.join("Bender.lock"))
        .into_diagnostic()
        .wrap_err_with(|| format!("Cannot open lockfile {:?}.", sess.root))?;
    let mut locked: Locked = serde_yaml_ng::from_reader(&file)
        .into_diagnostic()
        .wrap_err_with(|| format!("Syntax error in lockfile {:?}.", sess.root))?;

    for (name, url, hash) in &snapshot_list {
        let mut mod_package = locked.packages.get_mut(name).unwrap().clone();
        mod_package.revision = Some(hash.to_string());
        mod_package.version = None;
        mod_package.source = LockedSource::Git(url.to_string());
        locked.packages.insert(name.to_string(), mod_package);
    }

    for (path_dep, path_dep_path) in &path_subdeps {
        let mut mod_package = locked.packages[path_dep].clone();
        mod_package.revision = None;
        mod_package.version = None;
        mod_package.source = LockedSource::Path(
            path_dep_path
                .strip_prefix(sess.root)
                .unwrap_or(path_dep_path)
                .to_path_buf(),
        );
        locked.packages.insert(path_dep.clone(), mod_package);
    }

    let file = File::create(sess.root.join("Bender.lock"))
        .into_diagnostic()
        .wrap_err_with(|| format!("Cannot create lockfile {:?}.", sess.root))?;
    serde_yaml_ng::to_writer(&file, &locked)
        .into_diagnostic()
        .wrap_err_with(|| format!("Cannot write lockfile {:?}.", sess.root))?;

    if args.checkout {
        sess.load_locked(&locked)?;

        let rt = Runtime::new().into_diagnostic()?;
        let io = SessionIo::new(sess);
        let _srcs = rt.block_on(io.sources(args.force, &[]))?;
    }

    let snapshotted_deps = snapshot_list
        .iter()
        .map(|(name, _, _)| name.as_str())
        .collect::<Vec<&str>>();

    let subdeps = path_subdeps
        .iter()
        .map(|(name, _)| name.as_str())
        .collect::<Vec<&str>>();

    let updated_deps: Vec<&str> = [snapshotted_deps.clone(), subdeps].concat();

    // Update any possible workspace symlinks
    for (link_path, pkg_name) in &sess.manifest.workspace.package_links {
        if updated_deps.contains(&pkg_name.as_str()) {
            log::debug!("maintaining link to {} at {:?}", pkg_name, link_path);

            // Determine the checkout path for this package.
            let pkg_path = if snapshotted_deps.contains(&pkg_name.as_str()) {
                &sess.get_depsource_path(
                    pkg_name,
                    &DependencySource::Git(
                        snapshot_list
                            .iter()
                            .find(|(n, _, _)| n == pkg_name)
                            .unwrap()
                            .1
                            .clone(),
                    ),
                )
            } else {
                path_subdeps.get(pkg_name).unwrap()
            };
            // let pkg_path = &path.join(path_mod).join(dep);
            let pkg_path = link_path
                .parent()
                .and_then(|path| pathdiff::diff_paths(pkg_path, path))
                .unwrap_or_else(|| pkg_path.into());

            // Check if there is something at the destination path that needs to be
            // removed.
            if link_path.exists() {
                let meta = link_path
                    .symlink_metadata()
                    .into_diagnostic()
                    .wrap_err_with(|| {
                        format!("Failed to read metadata of path {:?}.", link_path)
                    })?;
                if !meta.file_type().is_symlink() {
                    Warnings::SkippingPackageLink(pkg_name.clone(), link_path.to_path_buf()).emit();
                    continue;
                }
                if link_path.read_link().map(|d| d != pkg_path).unwrap_or(true) {
                    log::debug!("removing existing link {:?}", link_path);
                    remove_symlink_dir(link_path).wrap_err_with(|| {
                        format!("Failed to remove symlink at path {:?}.", link_path)
                    })?;
                }
            }

            // Create the symlink if there is nothing at the destination.
            if !link_path.exists() {
                if let Some(parent) = link_path.parent() {
                    std::fs::create_dir_all(parent)
                        .into_diagnostic()
                        .wrap_err_with(|| format!("Failed to create directory {:?}.", parent))?;
                }
                let previous_dir = match link_path.parent() {
                    Some(parent) => {
                        let d = std::env::current_dir().unwrap();
                        std::env::set_current_dir(parent).unwrap();
                        Some(d)
                    }
                    None => None,
                };
                symlink_dir(&pkg_path, link_path).wrap_err_with(|| {
                    format!(
                        "Failed to create symlink to {:?} at path {:?}.",
                        pkg_path, link_path
                    )
                })?;
                if let Some(d) = previous_dir {
                    std::env::set_current_dir(d).unwrap();
                }
                stageln!(
                    "Linked",
                    "{} to {}",
                    fmt_pkg!(pkg_name),
                    fmt_path!(link_path.display())
                );
            }
        }
    }

    Ok(())
}
