use std::io::Write;
use std::path::PathBuf;

use crate::repository::Repository;
use anyhow::bail;
use clap::Clap;
use libpijul::change::ChangeHeader;
use libpijul::{Base32, MutTxnT, MutTxnTExt, TxnT};

#[derive(Clap, Debug)]
pub struct Tag {
    /// Set the repository where this command should run. Defaults to
    /// the first ancestor of the current directory that contains a
    /// `.pijul` directory.
    #[clap(long = "repository")]
    repo_path: Option<PathBuf>,
    #[clap(subcommand)]
    subcmd: SubCommand,
}

#[derive(Clap, Debug)]
pub enum SubCommand {
    /// Create a tag.
    #[clap(name = "create")]
    Create {
        #[clap(short = 'm', long = "message")]
        message: Option<String>,
        /// Set the author field
        #[clap(long = "author")]
        author: Option<String>,
        /// Record the change in this channel instead of the current channel
        #[clap(long = "channel")]
        channel: Option<String>,
        #[clap(long = "timestamp")]
        timestamp: Option<i64>,
    },
    /// Restore a tag into a new channel.
    #[clap(name = "checkout")]
    Checkout {
        tag: String,
        /// Optional new channel name. If not given, the base32
        /// representation of the tag hash is used.
        #[clap(long = "to-channel")]
        to_channel: Option<String>,
    },
}

impl Tag {
    pub async fn run(self) -> Result<(), anyhow::Error> {
        let mut stdout = std::io::stdout();
        let mut repo = Repository::find_root(self.repo_path).await?;
        match self.subcmd {
            SubCommand::Create {
                message,
                author,
                channel,
                timestamp,
            } => {
                let channel_name = repo
                    .config
                    .get_current_channel(channel.as_deref())
                    .to_string();
                try_record(&mut repo, &channel_name)?;
                let txn = repo.pristine.txn_begin()?;
                let mut w = std::fs::File::create("tag")?;
                let header = header(author.as_deref(), message, timestamp)?;
                let h = libpijul::tag::from_channel(&txn, &channel_name, &header, &mut w)?;
                std::fs::rename("tag", h.to_base32())?;
                writeln!(stdout, "{}", h.to_base32())?;
            }
            SubCommand::Checkout { tag, to_channel } => {
                if libpijul::Hash::from_base32(tag.as_bytes()).is_none() {
                    bail!("Invalid tag {:?}", tag)
                };
                let channel_name = if let Some(ref channel) = to_channel {
                    channel.as_str()
                } else {
                    tag.as_str()
                };
                let mut txn = repo.pristine.mut_txn_begin();
                if txn.load_channel(channel_name)?.is_some() {
                    bail!("Channel {:?} already exists", channel_name)
                }
                let f = libpijul::tag::OpenTagFile::open(&tag)?;
                libpijul::tag::restore_channel(f, &mut txn, &channel_name)?;
                txn.commit()?;
                repo.lock.commit().await?;
            }
        }
        Ok(())
    }
}

fn header(
    author: Option<&str>,
    message: Option<String>,
    timestamp: Option<i64>,
) -> Result<ChangeHeader, anyhow::Error> {
    let authors = if let Some(a) = author {
        vec![libpijul::change::Author {
            name: a.to_string(),
            full_name: None,
            email: None,
        }]
    } else if let Ok(global) = crate::config::Global::load() {
        vec![global.author]
    } else {
        Vec::new()
    };
    let header = ChangeHeader {
        message: message.clone().unwrap_or_else(String::new),
        authors,
        description: None,
        timestamp: if let Some(t) = timestamp {
            chrono::DateTime::from_utc(chrono::NaiveDateTime::from_timestamp(t, 0), chrono::Utc)
        } else {
            chrono::Utc::now()
        },
    };
    let toml = toml::to_string_pretty(&header)?;
    loop {
        let bytes = edit::edit_bytes(toml.as_bytes())?;
        if let Ok(header) = toml::from_slice(&bytes) {
            return Ok(header);
        }
    }
}

fn try_record(repo: &mut Repository, channel: &str) -> Result<(), anyhow::Error> {
    let mut txn = repo.pristine.mut_txn_begin();
    if let Some(mut channel) = txn.load_channel(channel)? {
        let mut state = libpijul::RecordBuilder::new();
        txn.record(
            &mut state,
            libpijul::Algorithm::default(),
            &mut channel,
            &mut repo.working_copy,
            &repo.changes,
            "",
        )?;
        let rec = state.finish();
        if !rec.actions.is_empty() {
            bail!("Cannot change channel, as there are unrecorded changes.")
        }
    }
    Ok(())
}
