use std::collections::HashMap;
use std::path::PathBuf;

use crate::repository::*;
use anyhow::bail;
use clap::Clap;
use libpijul::{MutTxnT, MutTxnTExt};
use log::debug;

#[derive(Clap, Debug)]
pub struct Clone {
    /// Only download changes with alive contents
    #[clap(long = "lazy")]
    lazy: bool,
    /// Set the remote channel
    #[clap(long = "channel", default_value = crate::DEFAULT_CHANNEL)]
    channel: String,
    /// Clone this change and its dependencies
    #[clap(long = "change", conflicts_with = "state")]
    change: Option<String>,
    /// Clone this state
    #[clap(long = "state", conflicts_with = "change")]
    state: Option<String>,
    /// Clone this path only
    #[clap(long = "path", multiple(true))]
    partial_paths: Vec<String>,
    /// Do not check certificates (HTTPS remotes only, this option might be dangerous)
    #[clap(short = 'k')]
    no_cert_check: bool,
    /// Clone this remote
    remote: String,
    /// Path where to clone the repository.
    /// If missing, the inferred name of the remote repository is used.
    path: Option<PathBuf>,
}

impl Clone {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let mut remote =
            crate::remote::unknown_remote(None, &self.remote, &self.channel, self.no_cert_check)
                .await?;

        let path = if let Some(path) = self.path {
            if path.is_relative() {
                let mut p = std::env::current_dir()?;
                p.push(path);
                p
            } else {
                path
            }
        } else if let Some(path) = remote.repo_name() {
            let mut p = std::env::current_dir()?;
            p.push(path);
            p
        } else {
            bail!("Could not infer repository name from {:?}", self.remote)
        };
        debug!("path = {:?}", path);

        let path_ = path.clone();
        ctrlc::set_handler(move || {
            std::fs::remove_dir_all(&path_).unwrap_or(());
            std::process::exit(130)
        })
        .unwrap_or(());

        let repo_path = RepoPath(path);
        let mut repo = Repository::init(Some(repo_path.0.clone()))?;
        let mut txn = repo.pristine.mut_txn_begin();
        let mut channel = txn.open_or_create_channel(&self.channel)?;
        if let Some(ref change) = self.change {
            let h = change.parse()?;
            remote
                .clone_tag(&mut repo, &mut txn, &mut channel, &[h])
                .await?
        } else if let Some(ref state) = self.state {
            let h = state.parse()?;
            remote
                .clone_state(&mut repo, &mut txn, &mut channel, h, self.lazy)
                .await?
        } else {
            remote
                .clone_channel(
                    &mut repo,
                    &mut txn,
                    &mut channel,
                    self.lazy,
                    &self.partial_paths,
                )
                .await?;
        }
        let progress = indicatif::ProgressBar::new_spinner();
        progress.set_style(
            indicatif::ProgressStyle::default_spinner().template("{spinner} Outputting repository"),
        );
        progress.enable_steady_tick(100);
        txn.output_repository_no_pending(
            &mut repo.working_copy,
            &repo.changes,
            &mut channel,
            &mut HashMap::new(),
            "",
            true,
            None,
        )?;
        remote.finish().await?;
        txn.commit()?;
        progress.set_style(
            indicatif::ProgressStyle::default_spinner().template("✓ Outputting repository"),
        );
        progress.finish();
        repo.config.current_channel = Some(self.channel);
        if let crate::remote::RemoteRepo::Local(ref l) = remote {
            repo.config.default_remote = std::fs::canonicalize(&l.root)?
                .to_str()
                .map(|x| x.to_string());
        } else {
            repo.config.default_remote = Some(self.remote);
        }
        repo.save_config()?;
        std::mem::drop(repo);
        std::mem::forget(repo_path);
        Ok(())
    }
}

struct RepoPath(PathBuf);

impl Drop for RepoPath {
    fn drop(&mut self) {
        std::fs::remove_dir_all(&self.0).unwrap_or(());
    }
}