use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::bail;
use canonical_path::CanonicalPathBuf;
use clap::Parser;
use jiff::Timestamp;
use log::debug;
use pijul_core::change::*;
use pijul_core::changestore::*;
use pijul_core::{
    ArcTxn, Base32, ChangeId, ChannelMutTxnT, ChannelRef, ChannelTxnT, GraphTxnT, MutTxnTExt, TxnT,
    TxnTExt,
};
use pijul_core::{HashMap, HashSet};

use crate::commands::common_opts::{DiffAlgorithm, RepoAndChannel};
use crate::commands::load_channel;

#[derive(Parser, Debug)]
pub struct Record {
    #[clap(flatten)]
    base: RepoAndChannel,
    /// Record all paths that have changed
    #[clap(short = 'a', long = "all")]
    pub all: bool,
    /// Set the change message
    #[clap(short = 'm', long = "message")]
    pub message: Option<String>,
    /// Set the description field.
    #[clap(long = "description")]
    pub description: Option<String>,
    /// Set the author field
    #[clap(long = "author")]
    pub author: Option<String>,
    /// Set the timestamp field
    #[clap(long = "timestamp")]
    pub timestamp: Option<Timestamp>,
    /// Amend this change instead of creating a new change
    #[clap(long = "amend")]
    #[allow(clippy::option_option)]
    pub amend: Option<Option<String>>,
    /// Paths in which to record the changes
    pub prefixes: Vec<PathBuf>,
    /// Identity to sign changes with
    #[clap(long = "identity")]
    pub identity: Option<String>,
    /// Diff algorithm to use (default: myers)
    #[clap(long = "algorithm", value_enum)]
    pub algorithm: Option<DiffAlgorithm>,
}

impl Record {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub async fn run(mut self, config: &pijul_config::Config) -> Result<(), anyhow::Error> {
        let mut repo = self.base.find_root()?;
        let mut stdout = std::io::stdout();
        let mut stderr = std::io::stderr();
        for h in config.hooks.record.iter() {
            h.run(repo.path.clone())?
        }
        let txn = repo.pristine.arc_txn_begin()?;
        let mut touched_inodes = pijul_core::unrecord::TouchedInodes::new();

        let (mut channel, extra) = {
            let txn = txn.read();

            let (channel, _) = load_channel(self.base.channel(), &*txn)?;

            let mut extra = Vec::new();
            for h in config.extra_dependencies.iter() {
                let (h, c) = txn.hash_from_prefix(h)?;
                if txn
                    .get_changeset(txn.changes(&*channel.read()), &c)?
                    .is_none()
                {
                    bail!(
                        "Change {:?} (from .pijul/config) is not on channel {:?}",
                        h,
                        channel.read().name
                    )
                }
                extra.push(h)
            }

            (channel, extra)
        };

        let header = if let Some(ref amend) = self.amend {
            let h = {
                let txn = txn.read();

                if let Some(hash) = amend {
                    txn.hash_from_prefix(hash)?.0
                } else if let Some(h) = txn.reverse_log(&*channel.read(), None)?.next() {
                    h?.1.0.into()
                } else {
                    return Ok(());
                }
            };
            let header = if let Some(message) = self.message.clone() {
                ChangeHeader {
                    message,
                    ..repo.changes.get_header(&h)?
                }
            } else {
                repo.changes.get_header(&h)?
            };

            let mut txn = txn.write();
            txn.unrecord(
                &repo.changes,
                &mut channel,
                &h,
                self.timestamp.map(Timestamp::as_second).unwrap_or(0) as u64,
                &mut touched_inodes,
            )?;
            txn.touch_inodes(&mut repo.working_copy, &mut touched_inodes)?;
            header
        } else {
            self.header(config).await?
        };
        let no_prefixes = self.prefixes.is_empty();
        let repo_path = CanonicalPathBuf::canonicalize(&repo.path)?;

        let complete =
            pijul_identity::Complete::load(&pijul_identity::choose_identity_name(config).await?)?;

        let secret = complete.skey();

        txn.write()
            .apply_root_change_if_needed(&repo.changes, &channel, rand::rng())?;

        let result = self.record(
            txn.clone(),
            channel.clone(),
            &repo.working_copy,
            &repo.changes,
            repo_path,
            header,
            &extra,
        )?;
        match result {
            Some((mut change, updates, oldest, actions_before_edit)) => {
                let hash_for_sig = change.hash()?;
                let sig_pem = pijul_identity::sign_pem(&secret, &hash_for_sig.to_bytes()).await?;
                change.unhashed = Some(serde_json::json!({ "signature": sig_pem }));
                let hash = repo
                    .changes
                    .save_change(&mut change, |_, _| Ok::<_, anyhow::Error>(()))?;

                let mut txn_ = txn.write();
                if !change.hashed.header.message.is_empty() {
                    txn_.apply_local_change(&mut channel, &change, &hash, &updates)?;

                    if !config.pins.is_empty() {
                        let change_id = *txn_
                            .get_internal(&(&hash).into())?
                            .expect("just-applied change not found in pristine");
                        let pins = config.pins.clone();
                        let mut ch = channel.write();
                        pijul_core::pin::reorder_pinned(&mut *txn_, &mut *ch, change_id, |h| {
                            pins.iter().any(|p| *p == h.to_base32())
                        })
                        .map_err(|e| anyhow::anyhow!("{e}"))?;
                    }

                    let mut path = repo.path.join(pijul_core::DOT_DIR);
                    path.push("identities");
                    std::fs::create_dir_all(&path)?;
                    writeln!(stdout, "Hash: {}", hash.to_base32())?;
                }

                debug!("oldest = {:?}", oldest);
                if no_prefixes {
                    let mut oldest = oldest
                        .duration_since(std::time::SystemTime::UNIX_EPOCH)
                        .unwrap()
                        .as_millis() as u64;
                    if oldest == 0 {
                        // If no diff was done at all, it means that no
                        // existing file changed since last time (some
                        // files may have been added, deleted or moved,
                        // but `touch` isn't about those).
                        oldest = std::time::SystemTime::now()
                            .duration_since(std::time::SystemTime::UNIX_EPOCH)
                            .unwrap()
                            .as_millis() as u64;
                    }
                    txn_.touch_channel(&mut *channel.write(), Some(oldest + 1));
                    let mut actions_after_edit = HashMap::new();
                    for i in change.hashed.changes.iter() {
                        for i in i.iter() {
                            match i {
                                Atom::NewVertex(n) => {
                                    actions_after_edit
                                        .entry(n.inode)
                                        .or_insert(HashSet::new())
                                        .insert(Atom::NewVertex(n.clone()));
                                }
                                Atom::EdgeMap(e) => {
                                    actions_after_edit
                                        .entry(e.inode)
                                        .or_insert(HashSet::new())
                                        .insert(Atom::EdgeMap(e.clone()));
                                }
                            }
                        }
                    }

                    let mut touched = pijul_core::unrecord::TouchedInodes::new();
                    for (i, x) in actions_before_edit.iter() {
                        if let Some(y) = actions_after_edit.get(i) {
                            if x != y {
                                touched.insert((ChangeId::ROOT, *i));
                            }
                        } else {
                            touched.insert((ChangeId::ROOT, *i));
                        }
                    }
                    debug!("touch {:?}", touched);
                    txn_.touch_inodes(&mut repo.working_copy, &touched)?;
                }

                std::mem::drop(txn_);

                txn.commit()?;
            }
            None => {
                if no_prefixes {
                    txn.write().touch_channel(&mut *channel.write(), None);
                    txn.commit()?;
                }
                writeln!(stderr, "Nothing to record")?;
            }
        }
        Ok(())
    }

    async fn header(&self, config: &pijul_config::Config) -> Result<ChangeHeader, anyhow::Error> {
        let mut authors = Vec::new();
        let mut b = std::collections::BTreeMap::new();
        if let Some(ref a) = self.author {
            b.insert("name".to_string(), a.clone());
        } else {
            let identity_name = self
                .identity
                .clone()
                .unwrap_or(pijul_identity::choose_identity_name(config).await?);

            let public_key = pijul_identity::public_key(&identity_name);
            b.insert("key".to_string(), public_key?.key);
        }

        authors.push(Author(b));
        let templates = config.template.as_ref();
        let message = if let Some(message) = &self.message {
            message.clone()
        } else if let Some(message_file) = templates.and_then(|t| t.message.as_ref()) {
            match std::fs::read_to_string(message_file) {
                Ok(m) => m,
                Err(e) => bail!("Could not read message template: {:?}: {}", message_file, e),
            }
        } else {
            String::new()
        };
        let description = if let Some(description) = &self.description {
            Some(description.clone())
        } else if let Some(descr_file) = templates.and_then(|t| t.description.as_ref()) {
            match std::fs::read_to_string(descr_file) {
                Ok(d) => Some(d),
                Err(e) => bail!(
                    "Could not read description template: {:?}: {}",
                    descr_file,
                    e
                ),
            }
        } else {
            None
        };
        let header = ChangeHeader {
            message,
            authors,
            description,
            timestamp: self.timestamp.unwrap_or_else(Timestamp::now),
        };
        Ok(header)
    }

    fn fill_relative_prefixes(&mut self) -> Result<(), anyhow::Error> {
        let cwd = std::env::current_dir()?;
        for p in self.prefixes.iter_mut() {
            if p.is_relative() {
                *p = cwd.join(&p);
            }
        }
        Ok(())
    }

    fn record<
        T: TxnTExt + MutTxnTExt + Sync + Send + 'static,
        C: ChangeStore + Send + Clone + 'static,
    >(
        mut self,
        txn: ArcTxn<T>,
        channel: ChannelRef<T>,
        working_copy: &pijul_core::working_copy::FileSystem,
        changes: &C,
        repo_path: CanonicalPathBuf,
        header: ChangeHeader,
        extra_deps: &[pijul_core::Hash],
    ) -> Result<
        Option<(
            Change,
            HashMap<usize, pijul_core::InodeUpdate>,
            std::time::SystemTime,
            HashMap<
                pijul_core::pristine::Position<Option<pijul_core::Hash>>,
                HashSet<pijul_core::change::Atom<Option<pijul_core::Hash>>>,
            >,
        )>,
        anyhow::Error,
    > {
        let mut state = pijul_core::RecordBuilder::new();
        let algorithm = match self.algorithm {
            Some(DiffAlgorithm::Patience) => pijul_core::Algorithm::Patience,
            Some(DiffAlgorithm::Histogram) => pijul_core::Algorithm::ImaraHistogram,
            None => pijul_core::Algorithm::default(),
        };
        if self.prefixes.is_empty() {
            state.record(
                txn.clone(),
                algorithm,
                false,
                &pijul_core::DEFAULT_SEPARATOR,
                channel.clone(),
                working_copy,
                changes,
                "",
                1, // std::thread::available_parallelism()?.get(),
            )?
        } else {
            self.fill_relative_prefixes()?;
            working_copy.record_prefixes(
                txn.clone(),
                algorithm,
                channel.clone(),
                changes,
                &mut state,
                repo_path,
                &self.prefixes,
                false,
                1, // std::thread::available_parallelism()?.get(),
                self.timestamp.map(Timestamp::as_second).unwrap_or(0) as u64,
            )?;
        }

        let mut rec = state.finish();
        if rec.actions.is_empty() {
            return Ok(None);
        }

        if rec.has_binary_files && !self.all {
            bail!("Cannot record a binary change interactively. Please use -a.")
        }

        debug!("TAKING LOCK {}", line!());
        let txn_ = txn.write();
        let actions: Vec<_> = rec
            .actions
            .into_iter()
            .map(|rec| rec.globalize(&*txn_).unwrap())
            .collect();

        let mut actions_before_edit = HashMap::new();
        for i in actions.iter() {
            for i in i.iter() {
                match i {
                    Atom::NewVertex(n) => {
                        actions_before_edit
                            .entry(n.inode)
                            .or_insert(HashSet::new())
                            .insert(Atom::NewVertex(n.clone()));
                    }
                    Atom::EdgeMap(e) => {
                        actions_before_edit
                            .entry(e.inode)
                            .or_insert(HashSet::new())
                            .insert(Atom::EdgeMap(e.clone()));
                    }
                }
            }
        }

        debug!("globalize done");
        let contents = if let Ok(c) = Arc::try_unwrap(rec.contents) {
            c.into_inner()
        } else {
            unreachable!()
        };
        let mut change =
            LocalChange::make_change(&*txn_, &channel, actions, contents, header, Vec::new())?;
        debug!("make change done");
        let current: HashSet<_> = change.dependencies.iter().cloned().collect();
        for dep in extra_deps.iter() {
            if !current.contains(dep) {
                change.dependencies.push(*dep)
            }
        }

        debug!("has_binary = {:?}", rec.has_binary_files);
        let mut change = if self.all {
            change
        } else {
            let mut o = Vec::new();
            debug!("write change");
            change.write(changes, None, true, &mut o)?;
            debug!("write change done");

            let mut with_errors: Option<Vec<u8>> = None;
            let change = loop {
                let mut bytes = if let Some(ref o) = with_errors {
                    edit::edit_bytes_with_builder(
                        &o[..],
                        tempfile::Builder::new().suffix(".pijul-commit"),
                    )?
                } else {
                    edit::edit_bytes_with_builder(
                        &o[..],
                        tempfile::Builder::new().suffix(".pijul-commit"),
                    )?
                };
                if bytes.iter().all(|c| (*c as char).is_whitespace()) {
                    bail!("Empty change")
                }
                let mut change = std::io::BufReader::new(std::io::Cursor::new(&bytes));
                if let Ok(change) =
                    Change::read_and_deps(&mut change, &mut rec.updatables, &*txn_, &channel)
                {
                    break change;
                }

                let mut err = SYNTAX_ERROR.as_bytes().to_vec();
                err.append(&mut bytes);
                with_errors = Some(err)
            };
            if change.changes.is_empty() {
                bail!("Cannot parse change")
            }
            change
        };

        let mut actions_after_edit = HashMap::new();
        for i in change.hashed.changes.iter() {
            for i in i.iter() {
                match i {
                    Atom::NewVertex(n) => {
                        actions_after_edit
                            .entry(n.inode)
                            .or_insert(HashSet::new())
                            .insert(Atom::NewVertex(n.clone()));
                    }
                    Atom::EdgeMap(e) => {
                        actions_after_edit
                            .entry(e.inode)
                            .or_insert(HashSet::new())
                            .insert(Atom::EdgeMap(e.clone()));
                    }
                }
            }
        }

        let current: HashSet<_> = change.dependencies.iter().cloned().collect();
        for dep in extra_deps.iter() {
            if !current.contains(dep) {
                change.dependencies.push(*dep)
            }
        }

        Ok(Some((
            change,
            rec.updatables,
            rec.oldest_change,
            actions_before_edit,
        )))
    }
}

const SYNTAX_ERROR: &str = "# Syntax errors, please try again.
# Alternatively, you may delete the entire file (including this
# comment) to abort.
";
