// SPDX-License-Identifier: GPL-2.0-only

//! `stg email send` implementation.

use std::{path::Path, str::FromStr};

use anyhow::{anyhow, Result};
use clap::Arg;

use crate::{
    argset,
    commit::CommitExtended,
    patchrange,
    stack::{Error, InitializationPolicy, Stack, StackAccess, StackStateAccess},
    stupid::Stupid,
};

pub(super) fn command() -> clap::Command {
    clap::Command::new("send")
        .about("Send patches as emails")
        .long_about(
            "Send patches as emails.\n\
             \n\
             This is a wrapper for `git send-email`. Refer to the git-send-email(1) \
             man page for additional details.\n\
             \n\
             The patches to send may be specified as files or directories generated by \
             `stg email format`, or as patch names/ranges as would be supplied to `stg \
             email format`. Specifying a directory will send all files in that \
             directory.\n\
             \n\
             The header of the email is configurable via command line options. The \
             user will be prompted for any necessary information not specified on the \
             command line or in the configuration.\n\
             \n\
             Many aspects of the send behavior may be controlled via the `sendemail.*` \
             configuration options. In particular, it is recommended to statically \
             configure SMTP details such as `sendemail.smtpServer`, \
             `sendemail.smtpUser`, etc. Refer to git-config(1) and git-send-email(1) \
             man pages for more detail on all the available configuration options.",
        )
        .override_usage(
            "stg email send [OPTIONS] <file|directory>...\n       \
             stg email send [OPTIONS] <patch>...\n       \
             stg email send [OPTIONS] --all\n       \
             stg email send --dump-aliases",
        )
        .arg(
            Arg::new("patchranges-or-paths")
                .help("Patch files, directory of patch files, or patch names to send")
                .long_help(
                    "Patches to send. May be patch files (as generated by `stg email \
                     format` or `git format-patch`), directories (which will send all \
                     files in each directory), or patch names from the series.\n\
                     \n\
                     Patch names may be specified individually or as ranges, but the \
                     specified patches must be contiguous.\n\
                     \n\
                     If file or directory names are ambiguous with patch names, the \
                     file or directory names will be used.",
                )
                .value_name("source")
                .num_args(1..)
                .value_parser(clap::builder::NonEmptyStringValueParser::new())
                .conflicts_with_all(["all", "dump-aliases"])
                .required_unless_present_any(["all", "dump-aliases"]),
        )
        .arg(argset::branch_arg())
        .arg(
            Arg::new("all")
                .long("all")
                .short('a')
                .help("Send all applied patches")
                .action(clap::ArgAction::SetTrue)
                .conflicts_with_all(["patchranges-or-paths", "dump-aliases"]),
        )
        .arg(
            Arg::new("git-send-email-opt")
                .long("git-opt")
                .short('G')
                .help("Pass additional <option> to `git send-email`")
                .long_help(
                    "Pass additional <option> to `git send-email`.\n\
                     \n\
                     See the git-send-email(1) man page. This option may be specified \
                     multiple times.",
                )
                .allow_hyphen_values(true)
                .action(clap::ArgAction::Append)
                .value_name("option"),
        )
        .next_help_heading("Compose Options")
        .args(compose_options())
        .next_help_heading("Send Options")
        .next_help_heading("Automate Options")
        .args(automate_options())
        .next_help_heading("Administer Options")
        .args(administer_options())
        .next_help_heading("Format Options")
        .args(format_options())
}

fn compose_options() -> Vec<Arg> {
    vec![
        Arg::new("from")
            .long("from")
            .help("Specify the \"From:\" address for each email")
            .long_help(
                "Specify the sender of the emails. If not specified on the command \
                 line, the value of the sendemail.from configuration option is used. \
                 If neither the command-line option nor sendemail.from are set, then \
                 the user will be prompted for the value. The default for the prompt \
                 will be the value of GIT_AUTHOR_IDENT, or GIT_COMMITTER_IDENT if that \
                 is not set, as returned by `git var -l`.",
            )
            .value_name("address")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new())
            .value_hint(clap::ValueHint::EmailAddress),
        Arg::new("to")
            .long("to")
            .help("Specify the \"To:\" address for each email")
            .long_help(
                "Specify the primary recipient of the emails generated. Generally, \
                 this will be the upstream maintainer of the project involved. Default \
                 is the value of the sendemail.to configuration value; if that is \
                 unspecified, and '--to-cmd' is not specified, this will be prompted \
                 for.\n\
                 \n\
                 This option may be specified multiple times.",
            )
            .value_name("address")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new())
            .action(clap::ArgAction::Append)
            .value_hint(clap::ValueHint::EmailAddress),
        Arg::new("cc")
            .long("cc")
            .help("Specify a \"Cc:\" address for each email")
            .long_help(
                "Specify a starting \"Cc:\" value for each email. Default is the value \
                 of sendemail.cc.\n\
                 \n\
                 This option may be specified multiple times.",
            )
            .value_name("address")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new())
            .action(clap::ArgAction::Append)
            .value_hint(clap::ValueHint::EmailAddress),
        Arg::new("bcc")
            .long("bcc")
            .help("Specify a \"Bcc:\" address for each email")
            .long_help(
                "Specify a starting \"Bcc:\" value for each email. Default is the \
                 value of sendemail.bcc.\n\
                 \n\
                 This option may be specified multiple times.",
            )
            .value_name("address")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new())
            .action(clap::ArgAction::Append)
            .value_hint(clap::ValueHint::EmailAddress),
        Arg::new("subject")
            .long("subject")
            .help("Specify email \"Subject:\"")
            .long_help(
                "Specify the initial subject of the email thread. Only necessary if \
                 '--compose' is also set. If '--compose' is not set, this will be \
                 prompted for.",
            )
            .value_name("subject")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new()),
        Arg::new("reply-to")
            .long("reply-to")
            .help("Specify the \"Reply-To:\" address")
            .long_help(
                "Specify the address where replies from recipients should go to. Use \
                 this if replies to messages should go to another address than what is \
                 specified with the '--from' parameter.",
            )
            .value_name("address")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new())
            .value_hint(clap::ValueHint::EmailAddress),
        Arg::new("in-reply-to")
            .long("in-reply-to")
            .help("Specify the \"In-Reply-To:\" identifier")
            .long_help(
                "Make the first mail (or all the mails with '--no-thread') appear as a \
                 reply to the given Message-Id, which avoids breaking threads to \
                 provide a new patch series. The second and subsequent emails will be \
                 sent as replies according to the '--[no-]chain-reply-to' setting.\n\
                 \n\
                 So for example when '--thread' and '--no-chain-reply-to' are \
                 specified, the second and subsequent patches will be replies to the \
                 first one like in the illustration below where [PATCH v2 0/3] is in \
                 reply to [PATCH 0/2]:\n\
                 \n    [PATCH 0/2] Here is what I did...\
                 \n      [PATCH 1/2] Clean up and tests\
                 \n      [PATCH 2/2] Implementation\
                 \n      [PATCH v2 0/3] Here is a reroll\
                 \n        [PATCH v2 1/3] Clean up\
                 \n        [PATCH v2 2/3] New tests\
                 \n        [PATCH v2 3/3] Implementation\
                 \n\n\
                 Only necessary if '--compose' is also set. If '--compose' is not set, \
                 this will be prompted for.",
            )
            .value_name("id")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new()),
        Arg::new("compose")
            .long("compose")
            .help("Open an editor for introduction")
            .long_help(
                "Invoke a text editor (see GIT_EDITOR in git-var(1)) to edit an \
                 introductory message for the patch series.\n\
                 \n\
                 When '--compose' is used, git send-email will use the From, Subject, \
                 and In-Reply-To headers specified in the message. If the body of the \
                 message (what you type after the headers and a blank line) only \
                 contains blank (or Git: prefixed) lines, the summary will not be \
                 sent, but From, Subject, and In-Reply-To headers will be used unless \
                 they are removed.\n\
                 \n\
                 Missing From or In-Reply-To headers will be prompted for.\n\
                 \n\
                 See the CONFIGURATION section of git-send-email(1) for \
                 sendemail.multiEdit.",
            )
            .action(clap::ArgAction::SetTrue),
        Arg::new("annotate")
            .long("annotate")
            .help("Review each patch that will be sent in an editor")
            .long_help(
                "Review and edit each patch you are about to send. Default is the \
                 value of sendemail.annotate.",
            )
            .action(clap::ArgAction::SetTrue),
    ]
}

fn automate_options() -> Vec<Arg> {
    vec![
        Arg::new("identity")
            .long("identity")
            .help("Use the sendmail.<id> options")
            .long_help(
                "A configuration identity. When given, causes values in the \
                 sendemail.<identity> subsection to take precedence over values in the \
                 sendemail section. The default identity is the value of \
                 sendemail.identity.",
            )
            .value_name("id")
            .num_args(1)
            .value_parser(clap::builder::NonEmptyStringValueParser::new()),
        Arg::new("no-thread")
            .long("no-thread")
            .help("Do not add In-Reply-To and Reference headers to each email")
            .long_help(
                "If threading is enabled, the In-Reply-To and References headers will \
                 be added to each email sent. Whether each mail refers to the previous \
                 email (deep threading per `git format-patch` wording) or to the first \
                 email (shallow threading) is governed by \
                 '--[no-]chain-reply-to'.\n\
                 \n\
                 If disabled with '--no-thread', those headers will not be added \
                 (unless specified with '--in-reply-to'). Default is the value of the \
                 sendemail.thread configuration value; if that is unspecified, default \
                 to '--thread'.\n\
                 \n\
                 It is up to the user to ensure that no In-Reply-To header already \
                 exists when `git send-email` is asked to add it (especially note that \
                 `git format-patch` can be configured to do the threading itself). \
                 Failure to do so may not produce the expected result in the \
                 recipient’s MUA.",
            )
            .action(clap::ArgAction::SetTrue),
    ]
}

fn administer_options() -> Vec<Arg> {
    vec![
        Arg::new("confirm")
            .long("confirm")
            .help("Confirm recipients before sending")
            .long_help(
                "Confirm just before sending.\n\
                 \n\
                 Default is the value of sendemail.confirm configuration value; if \
                 that is unspecified, default to auto unless any of the suppress \
                 options have been specified, in which case default to compose.\n\
                 \n\
                 Confirmation modes:\n\
                 \n  - 'always' will always confirm before sending\
                 \n  - 'never' will never confirm before sending\
                 \n  - 'cc' will confirm before sending when send-email has\
                 \n    automatically added addresses from the patch to the Cc list\
                 \n  - 'compose' will confirm before sending the first message\
                 \n    when using --compose\
                 \n  - 'auto' is equivalent to cc + compose",
            )
            .hide_possible_values(true)
            .num_args(1)
            .value_name("mode")
            .value_parser(["always", "never", "cc", "compose", "auto"]),
        Arg::new("quiet")
            .long("quiet")
            .help("Output one line of info per email")
            .long_help(
                "Make git-send-email less verbose. One line per email should be all \
                 that is output.",
            )
            .action(clap::ArgAction::SetTrue),
        Arg::new("dry-run")
            .long("dry-run")
            .help("Do not actually send the emails")
            .long_help("Do everything except actually send the emails.")
            .action(clap::ArgAction::SetTrue),
        Arg::new("dump-aliases")
            .long("dump-aliases")
            .help("Dump configured aliases and exit")
            .action(clap::ArgAction::SetTrue),
    ]
}

fn format_options() -> Vec<Arg> {
    vec![
        Arg::new("numbered")
            .long("numbered")
            .short('n')
            .help("Use [PATCH n/m] even with a single patch")
            .action(clap::ArgAction::SetTrue),
        Arg::new("no-numbered")
            .long("no-numbered")
            .short('N')
            .help("Use [PATCH] even with multiple patches")
            .action(clap::ArgAction::SetTrue)
            .conflicts_with("numbered"),
        Arg::new("start-number")
            .long("start-number")
            .help("Start numbering at <n> instead of 1")
            .value_name("n")
            .num_args(1),
        Arg::new("reroll-count")
            .long("reroll-count")
            .short('v')
            .help("Mark the series as the <n>th reroll")
            .value_name("n")
            .num_args(1),
        Arg::new("rfc")
            .long("rfc")
            .help("Use [RFC PATCH] instead of [PATCH]")
            .action(clap::ArgAction::SetTrue),
        Arg::new("subject-prefix")
            .long("subject-prefix")
            .help("Use [<prefix>] instead of [PATCH]")
            .value_name("prefix")
            .num_args(1),
    ]
}

pub(super) fn dispatch(matches: &clap::ArgMatches) -> Result<()> {
    let repo = git2::Repository::open_from_env()?;

    if matches.get_flag("dump-aliases") {
        return repo.stupid().send_email_dump_aliases();
    }

    let stack = Stack::from_branch(
        &repo,
        argset::get_one_str(matches, "branch"),
        InitializationPolicy::AllowUninitialized,
    )?;

    let source_args = matches.get_many::<String>("patchranges-or-paths");
    let sources = if let Some(patchranges_or_paths) = source_args {
        let patchranges_or_paths = patchranges_or_paths.collect::<Vec<_>>();
        if patchranges_or_paths.iter().all(|s| Path::new(s).is_dir())
            || patchranges_or_paths.iter().all(|s| Path::new(s).is_file())
        {
            patchranges_or_paths
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
        } else {
            let mut specs = Vec::new();
            for arg in patchranges_or_paths {
                specs.push(patchrange::Specification::from_str(arg)?);
            }
            let patches = patchrange::contiguous_patches_from_specs(
                &specs,
                &stack,
                patchrange::Allow::VisibleWithAppliedBoundary,
            )?;
            if patches.is_empty() {
                return Err(anyhow!("No patches to send"));
            } else {
                for patchname in &patches {
                    if stack.get_patch_commit(patchname).is_no_change()? {
                        return Err(anyhow!("Cannot send empty patch `{patchname}`"));
                    }
                }
            }
            let base = stack.get_patch_commit(&patches[0]).parent_id(0)?;
            let last = stack.get_patch_commit(patches.last().unwrap()).id();
            vec![format!("{base}..{last}")]
        }
    } else if matches.get_flag("all") {
        let applied = stack.applied();
        if applied.is_empty() {
            return Err(Error::NoAppliedPatches.into());
        } else {
            for patchname in applied {
                if stack.get_patch_commit(patchname).is_no_change()? {
                    return Err(anyhow!("Cannot send empty patch `{patchname}`"));
                }
            }
        }
        let base = stack.base().id();
        let last = stack.get_patch_commit(applied.last().unwrap()).id();
        vec![format!("{base}..{last}")]
    } else {
        panic!("expect either patchranges or -a/--all")
    };

    let mut send_args = Vec::new();

    let mut dummy_command = clap::Command::new("dummy")
        .args(compose_options())
        .args(automate_options())
        .args(administer_options())
        .args(format_options());
    dummy_command.build();

    for arg in dummy_command.get_arguments() {
        let arg_id = arg.get_id().as_str();
        if matches!(
            matches.value_source(arg_id),
            Some(clap::parser::ValueSource::CommandLine)
        ) {
            let num_args = arg.get_num_args().expect("built Arg's num_args is Some");
            let long = arg.get_long().expect("passthrough arg has long option");
            let indices = matches.indices_of(arg_id).expect("value source is cmdline");
            if num_args.takes_values() {
                let values = matches.get_many::<String>(arg_id).unwrap();
                assert!(indices.len() == values.len());
                indices.into_iter().zip(values).for_each(|(index, value)| {
                    send_args.push((index, format!("--{long}={value}")));
                });
            } else {
                indices.for_each(|index| send_args.push((index, format!("--{long}"))));
            }
        }
    }

    send_args.sort_by_key(|(index, _)| *index);

    let mut send_args = send_args.drain(..).map(|(_, s)| s).collect::<Vec<_>>();

    if let Some(values) = matches.get_many::<String>("git-send-email-opt") {
        send_args.extend(values.cloned());
    }

    let mut sources = sources;
    send_args.append(&mut sources);

    repo.stupid().send_email(send_args)
}
