// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![expect(missing_docs)]

use std::sync::Arc;

use pollster::FutureExt as _;

use crate::backend;
use crate::backend::BackendError;
use crate::backend::BackendResult;
use crate::backend::ChangeId;
use crate::backend::CommitId;
use crate::backend::Signature;
use crate::backend::TreeId;
use crate::commit::Commit;
use crate::commit::is_backend_commit_empty;
use crate::conflict_labels::ConflictLabels;
use crate::merge::Merge;
use crate::merged_tree::MergedTree;
use crate::repo::MutableRepo;
use crate::repo::Repo;
use crate::settings::JJRng;
use crate::settings::SignSettings;
use crate::settings::UserSettings;
use crate::signing::SignBehavior;
use crate::store::Store;

#[must_use]
pub struct CommitBuilder<'repo> {
    mut_repo: &'repo mut MutableRepo,
    inner: DetachedCommitBuilder,
}

impl CommitBuilder<'_> {
    /// Detaches from `&'repo mut` lifetime. The returned builder can be used in
    /// order to obtain a temporary commit object.
    pub fn detach(self) -> DetachedCommitBuilder {
        self.inner
    }

    /// Clears the source commit to not record new commit as rewritten from it.
    ///
    /// The caller should also assign new change id to not create divergence.
    pub fn clear_rewrite_source(mut self) -> Self {
        self.inner.clear_rewrite_source();
        self
    }

    pub fn parents(&self) -> &[CommitId] {
        self.inner.parents()
    }

    pub fn set_parents(mut self, parents: Vec<CommitId>) -> Self {
        self.inner.set_parents(parents);
        self
    }

    pub fn predecessors(&self) -> &[CommitId] {
        self.inner.predecessors()
    }

    pub fn set_predecessors(mut self, predecessors: Vec<CommitId>) -> Self {
        self.inner.set_predecessors(predecessors);
        self
    }

    pub fn tree(&self) -> MergedTree {
        self.inner.tree()
    }

    pub fn tree_ids(&self) -> &Merge<TreeId> {
        self.inner.tree_ids()
    }

    pub fn set_tree(mut self, tree: MergedTree) -> Self {
        self.inner.set_tree(tree);
        self
    }

    /// [`Commit::is_empty()`] for the new commit.
    pub async fn is_empty(&self) -> BackendResult<bool> {
        self.inner.is_empty(self.mut_repo).await
    }

    pub fn change_id(&self) -> &ChangeId {
        self.inner.change_id()
    }

    pub fn set_change_id(mut self, change_id: ChangeId) -> Self {
        self.inner.set_change_id(change_id);
        self
    }

    pub fn generate_new_change_id(mut self) -> Self {
        self.inner.generate_new_change_id();
        self
    }

    pub fn description(&self) -> &str {
        self.inner.description()
    }

    pub fn set_description(mut self, description: impl Into<String>) -> Self {
        self.inner.set_description(description);
        self
    }

    pub fn author(&self) -> &Signature {
        self.inner.author()
    }

    pub fn set_author(mut self, author: Signature) -> Self {
        self.inner.set_author(author);
        self
    }

    pub fn committer(&self) -> &Signature {
        self.inner.committer()
    }

    pub fn set_committer(mut self, committer: Signature) -> Self {
        self.inner.set_committer(committer);
        self
    }

    /// [`Commit::is_discardable()`] for the new commit.
    pub async fn is_discardable(&self) -> BackendResult<bool> {
        self.inner.is_discardable(self.mut_repo).await
    }

    pub fn sign_settings(&self) -> &SignSettings {
        self.inner.sign_settings()
    }

    pub fn set_sign_behavior(mut self, sign_behavior: SignBehavior) -> Self {
        self.inner.set_sign_behavior(sign_behavior);
        self
    }

    pub fn set_sign_key(mut self, sign_key: String) -> Self {
        self.inner.set_sign_key(sign_key);
        self
    }

    pub fn clear_sign_key(mut self) -> Self {
        self.inner.clear_sign_key();
        self
    }

    pub async fn write(self) -> BackendResult<Commit> {
        self.inner.write(self.mut_repo).await
    }

    /// Records the old commit as abandoned instead of writing new commit. This
    /// is noop for the builder created by [`MutableRepo::new_commit()`].
    pub fn abandon(self) {
        self.inner.abandon(self.mut_repo);
    }
}

/// Like `CommitBuilder`, but doesn't mutably borrow `MutableRepo`.
#[derive(Debug)]
pub struct DetachedCommitBuilder {
    store: Arc<Store>,
    rng: Arc<JJRng>,
    commit: backend::Commit,
    predecessors: Vec<CommitId>,
    rewrite_source: Option<Commit>,
    sign_settings: SignSettings,
    record_predecessors_in_commit: bool,
}

impl DetachedCommitBuilder {
    /// Only called from [`MutableRepo::new_commit`]. Use that function instead.
    pub(crate) fn for_new_commit(
        repo: &dyn Repo,
        settings: &UserSettings,
        parents: Vec<CommitId>,
        tree: MergedTree,
    ) -> Self {
        let store = repo.store().clone();
        let signature = settings.signature();
        assert!(!parents.is_empty());
        let rng = settings.get_rng();
        let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels();
        let change_id = rng.new_change_id(store.change_id_length());
        let commit = backend::Commit {
            parents,
            predecessors: vec![],
            root_tree,
            conflict_labels: conflict_labels.into_merge(),
            change_id,
            description: String::new(),
            author: signature.clone(),
            committer: signature,
            secure_sig: None,
        };
        let record_predecessors_in_commit = settings
            .get_bool("experimental.record-predecessors-in-commit")
            .unwrap();
        Self {
            store,
            rng,
            commit,
            rewrite_source: None,
            predecessors: vec![],
            sign_settings: settings.sign_settings(),
            record_predecessors_in_commit,
        }
    }

    /// Only called from [`MutableRepo::rewrite_commit`]. Use that function
    /// instead.
    pub(crate) fn for_rewrite_from(
        repo: &dyn Repo,
        settings: &UserSettings,
        predecessor: &Commit,
    ) -> Self {
        let store = repo.store().clone();
        let mut commit = backend::Commit::clone(predecessor.store_commit());
        commit.predecessors = vec![];
        commit.committer = settings.signature();
        // If the user had not configured a name and email before but now they have,
        // update the author fields with the new information.
        if commit.author.name.is_empty() {
            commit.author.name.clone_from(&commit.committer.name);
        }
        if commit.author.email.is_empty() {
            commit.author.email.clone_from(&commit.committer.email);
        }

        // Reset author timestamp on discardable commits if the author is the
        // committer. While it's unlikely we'll have somebody else's commit
        // with no description in our repo, we'd like to be extra safe.
        if commit.author.name == commit.committer.name
            && commit.author.email == commit.committer.email
            && predecessor
                .is_discardable(repo)
                .block_on()
                .unwrap_or_default()
        {
            commit.author.timestamp = commit.committer.timestamp;
        }

        let record_predecessors_in_commit = settings
            .get_bool("experimental.record-predecessors-in-commit")
            .unwrap();
        Self {
            store,
            commit,
            rng: settings.get_rng(),
            rewrite_source: Some(predecessor.clone()),
            predecessors: vec![predecessor.id().clone()],
            sign_settings: settings.sign_settings(),
            record_predecessors_in_commit,
        }
    }

    /// Attaches the underlying `mut_repo`.
    pub fn attach(self, mut_repo: &mut MutableRepo) -> CommitBuilder<'_> {
        assert!(Arc::ptr_eq(&self.store, mut_repo.store()));
        CommitBuilder {
            mut_repo,
            inner: self,
        }
    }

    /// Clears the source commit to not record new commit as rewritten from it.
    ///
    /// The caller should also assign new change id to not create divergence.
    pub fn clear_rewrite_source(&mut self) {
        self.rewrite_source = None;
    }

    pub fn parents(&self) -> &[CommitId] {
        &self.commit.parents
    }

    pub fn set_parents(&mut self, parents: Vec<CommitId>) -> &mut Self {
        assert!(!parents.is_empty());
        self.commit.parents = parents;
        self
    }

    pub fn predecessors(&self) -> &[CommitId] {
        &self.predecessors
    }

    pub fn set_predecessors(&mut self, predecessors: Vec<CommitId>) -> &mut Self {
        self.predecessors = predecessors;
        self
    }

    pub fn tree(&self) -> MergedTree {
        MergedTree::new(
            self.store.clone(),
            self.commit.root_tree.clone(),
            ConflictLabels::from_merge(self.commit.conflict_labels.clone()),
        )
    }

    pub fn tree_ids(&self) -> &Merge<TreeId> {
        &self.commit.root_tree
    }

    pub fn set_tree(&mut self, tree: MergedTree) -> &mut Self {
        assert!(Arc::ptr_eq(tree.store(), &self.store));
        let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels();
        self.commit.root_tree = root_tree;
        self.commit.conflict_labels = conflict_labels.into_merge();
        self
    }

    /// [`Commit::is_empty()`] for the new commit.
    pub async fn is_empty(&self, repo: &dyn Repo) -> BackendResult<bool> {
        is_backend_commit_empty(repo, &self.store, &self.commit).await
    }

    pub fn change_id(&self) -> &ChangeId {
        &self.commit.change_id
    }

    pub fn set_change_id(&mut self, change_id: ChangeId) -> &mut Self {
        self.commit.change_id = change_id;
        self
    }

    pub fn generate_new_change_id(&mut self) -> &mut Self {
        self.commit.change_id = self.rng.new_change_id(self.store.change_id_length());
        self
    }

    pub fn description(&self) -> &str {
        &self.commit.description
    }

    pub fn set_description(&mut self, description: impl Into<String>) -> &mut Self {
        self.commit.description = description.into();
        self
    }

    pub fn author(&self) -> &Signature {
        &self.commit.author
    }

    pub fn set_author(&mut self, author: Signature) -> &mut Self {
        self.commit.author = author;
        self
    }

    pub fn committer(&self) -> &Signature {
        &self.commit.committer
    }

    pub fn set_committer(&mut self, committer: Signature) -> &mut Self {
        self.commit.committer = committer;
        self
    }

    /// [`Commit::is_discardable()`] for the new commit.
    pub async fn is_discardable(&self, repo: &dyn Repo) -> BackendResult<bool> {
        Ok(self.description().is_empty() && self.is_empty(repo).await?)
    }

    pub fn sign_settings(&self) -> &SignSettings {
        &self.sign_settings
    }

    pub fn set_sign_behavior(&mut self, sign_behavior: SignBehavior) -> &mut Self {
        self.sign_settings.behavior = sign_behavior;
        self
    }

    pub fn set_sign_key(&mut self, sign_key: String) -> &mut Self {
        self.sign_settings.key = Some(sign_key);
        self
    }

    pub fn clear_sign_key(&mut self) -> &mut Self {
        self.sign_settings.key = None;
        self
    }

    /// Writes new commit and makes it visible in the `mut_repo`.
    pub async fn write(mut self, mut_repo: &mut MutableRepo) -> BackendResult<Commit> {
        if self.record_predecessors_in_commit {
            self.commit.predecessors = self.predecessors.clone();
        }
        let commit = write_to_store(&self.store, self.commit, &self.sign_settings).await?;
        // FIXME: Google's index.has_id() always returns true.
        if mut_repo.is_backed_by_default_index()
            && mut_repo
                .index()
                .has_id(commit.id())
                // TODO: indexing error shouldn't be a "BackendError"
                .map_err(|err| BackendError::Other(err.into()))?
        {
            // Recording existing commit as new would create cycle in
            // predecessors/parent mappings within the current transaction, and
            // in predecessors graph globally.
            return Err(BackendError::Other(
                format!("Newly-created commit {id} already exists", id = commit.id()).into(),
            ));
        }
        mut_repo.add_head(&commit).await?;
        mut_repo.set_predecessors(commit.id().clone(), self.predecessors);
        if let Some(rewrite_source) = self.rewrite_source {
            mut_repo.set_rewritten_commit(rewrite_source.id().clone(), commit.id().clone());
        }
        Ok(commit)
    }

    /// Writes new commit without making it visible in the repo.
    ///
    /// This does not consume the builder, so you can reuse the current
    /// configuration to create another commit later.
    pub async fn write_hidden(&self) -> BackendResult<Commit> {
        let mut commit = self.commit.clone();
        if self.record_predecessors_in_commit {
            commit.predecessors = self.predecessors.clone();
        }
        write_to_store(&self.store, commit, &self.sign_settings).await
    }

    /// Records the old commit as abandoned in the `mut_repo`.
    ///
    /// This is noop if there's no old commit that would be rewritten to the new
    /// commit by `write()`.
    pub fn abandon(self, mut_repo: &mut MutableRepo) {
        let commit = self.commit;
        if let Some(rewrite_source) = &self.rewrite_source {
            mut_repo
                .record_abandoned_commit_with_parents(rewrite_source.id().clone(), commit.parents);
        }
    }
}

async fn write_to_store(
    store: &Arc<Store>,
    mut commit: backend::Commit,
    sign_settings: &SignSettings,
) -> BackendResult<Commit> {
    let should_sign = store.signer().can_sign() && sign_settings.should_sign(&commit);
    let sign_fn = |data: &[u8]| store.signer().sign(data, sign_settings.key.as_deref());

    // Commit backend doesn't use secure_sig for writing and enforces it with an
    // assert, but sign_settings.should_sign check above will want to know
    // if we're rewriting a signed commit
    commit.secure_sig = None;

    store
        .write_commit(commit, should_sign.then_some(&mut &sign_fn))
        .await
}
