// Copyright 2023 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.

use clap_complete::ArgValueCompleter;
use futures::TryStreamExt as _;
use indexmap::IndexSet;
use itertools::Itertools as _;
use jj_lib::commit::Commit;
use jj_lib::commit::CommitIteratorExt as _;
use jj_lib::repo::Repo as _;
use jj_lib::revset::RevsetStreamExt as _;
use jj_lib::signing::SignBehavior;

use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::cli_util::print_updated_commits;
use crate::cli_util::short_change_hash;
use crate::command_error::CommandError;
use crate::command_error::user_error;
use crate::complete;
use crate::progress::ProgressWriter;
use crate::ui::Ui;

/// Cryptographically sign a revision
///
/// This command requires configuring a [commit signing] backend.
///
/// [commit signing]:
///     https://docs.jj-vcs.dev/latest/config/#commit-signing
#[derive(clap::Args, Clone, Debug)]
pub struct SignArgs {
    /// What revision(s) to sign
    ///
    /// If no revisions are specified, this defaults to the `revsets.sign`
    /// setting.
    ///
    /// Note that revisions are always re-signed.
    ///
    /// While that leads to discomfort for users, which sign with hardware
    /// devices, as of now we cannot reliably check if a commit is already
    /// signed by the user without creating a signature (see [#5786]).
    ///
    /// [#5786]:
    ///     https://github.com/jj-vcs/jj/issues/5786
    #[arg(long = "revision", short, value_name = "REVSETS", alias = "revisions")]
    #[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]
    revisions: Vec<RevisionArg>,

    /// The key used for signing
    #[arg(long)]
    key: Option<String>,
}

pub async fn cmd_sign(
    ui: &mut Ui,
    command: &CommandHelper,
    args: &SignArgs,
) -> Result<(), CommandError> {
    let mut workspace_command = command.workspace_helper(ui)?;

    if !workspace_command.repo().store().signer().can_sign() {
        return Err(user_error(
            "No signing backend configured").hinted(
            "For configuring a signing backend, see https://docs.jj-vcs.dev/latest/config/#commit-signing",
        ));
    }

    let revset_expression = if args.revisions.is_empty() {
        let revset_string = workspace_command.settings().get_string("revsets.sign")?;
        workspace_command.parse_revset(ui, &RevisionArg::from(revset_string))?
    } else {
        workspace_command.parse_union_revsets(ui, &args.revisions)?
    }
    .resolve()?;

    workspace_command
        .check_rewritable_expr(&revset_expression)
        .await?;

    let to_sign: IndexSet<Commit> = revset_expression
        .evaluate(workspace_command.repo().as_ref())?
        .stream()
        .commits(workspace_command.repo().store())
        .try_collect()
        .await?;

    let mut tx = workspace_command.start_transaction();

    let mut signed_commits = vec![];
    let mut num_reparented = 0;

    {
        let mut progress_writer = ProgressWriter::new(ui, "Signing");

        tx.repo_mut()
            .transform_descendants(
                to_sign.iter().ids().cloned().collect_vec(),
                async |rewriter| {
                    let old_commit = rewriter.old_commit().clone();
                    let mut commit_builder = rewriter.reparent();

                    if let Some(writer) = &mut progress_writer {
                        writer
                            .display(&short_change_hash(old_commit.change_id()))
                            .ok();
                    }

                    if to_sign.contains(&old_commit) {
                        if let Some(key) = &args.key {
                            commit_builder = commit_builder.set_sign_key(key.clone());
                        }

                        let new_commit = commit_builder
                            .set_sign_behavior(SignBehavior::Force)
                            .write()
                            .await?;

                        signed_commits.push(new_commit);
                    } else {
                        commit_builder.write().await?;
                        num_reparented += 1;
                    }

                    Ok(())
                },
            )
            .await?;
    }

    if let Some(mut formatter) = ui.status_formatter()
        && !signed_commits.is_empty()
    {
        writeln!(formatter, "Signed {} commits:", signed_commits.len())?;
        print_updated_commits(
            formatter.as_mut(),
            &tx.commit_summary_template(),
            &signed_commits,
        )?;
    }

    let num_not_authored_by_me = signed_commits
        .iter()
        .filter(|commit| commit.author().email != tx.settings().user_email())
        .count();
    if num_not_authored_by_me > 0 {
        writeln!(
            ui.warning_default(),
            "{num_not_authored_by_me} of these commits are not authored by you",
        )?;
    }

    if num_reparented > 0 {
        writeln!(ui.status(), "Rebased {num_reparented} descendant commits")?;
    }

    let transaction_description = match &*signed_commits {
        [] => "".to_string(),
        [commit] => format!("sign commit {}", commit.id()),
        commits => format!(
            "sign commit {} and {} more",
            commits[0].id(),
            commits.len() - 1
        ),
    };
    tx.finish(ui, transaction_description).await?;

    Ok(())
}
