1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
use clap::{ArgMatches, Command};
use seaplane::{api::ApiErrorKind, error::SeaplaneError};

use crate::{
    api::FormationsReq,
    cli::{
        cmds::{flight::SeaplaneFlightDelete, formation::SeaplaneFormationFetch},
        errors,
        validator::{validate_formation_name, validate_name_id},
        CliCommand,
    },
    context::Ctx,
    error::{CliErrorKind, Context, Result},
    printer::Color,
};

#[derive(Copy, Clone, Debug)]
pub struct SeaplaneFormationDelete;

impl SeaplaneFormationDelete {
    pub fn command() -> Command<'static> {
        let validator = |s: &str| validate_name_id(validate_formation_name, s);
        // TODO: add --recursive to handle configurations too
        Command::new("delete")
            .visible_aliases(&["del", "remove", "rm"])
            .about("Deletes local Formation Plans and/or remote Formation Instances")
            .override_usage(
                "seaplane formation delete <NAME|ID> [OPTIONS]
    seaplane formation delete <NAME|ID> --no-remote [OPTIONS]")
            .arg(arg!(formation =["NAME|ID"] required)
                .validator(validator)
                .help("The name or ID of the Formation to remove, must be unambiguous"))
            .arg(arg!(--recursive -('r'))
                .help("Recursively delete all local definitions associated with this Formation"))
            .arg(arg!(--force -('f'))
                .help("Delete this Formation even if there are remote instances In Flight (active), which will effectively stop all remote instances of this Formation"))
            .arg(arg!(--all -('a'))
                .help("Delete all matching Formations even when the name or ID is ambiguous or a partial match"))
            .arg(arg!(--local)
                .overrides_with("no-local")
                .help("Delete local Formation Definitions (this is set by the default, use --no-local to skip)"))
            .arg(arg!(--("no-local"))
                .overrides_with("local")
                .help("DO NOT delete local Formation Definitions"))
            .arg(arg!(--remote)
                .overrides_with("no-remote")
                .help("Delete remote Formation Instances (this is set by default, use --no-remote to skip)"))
            .arg(arg!(--("no-remote"))
                .overrides_with("remote")
                .help("DO NOT delete remote Formation Instances (this is set by the default, use --remote to remove them)"))
            .arg(arg!(--fetch|sync|synchronize - ('F')).help(
                "Fetch remote Formation Instances and synchronize local Plan definitions prior to attempting to delete",
            ))
    }
}

impl CliCommand for SeaplaneFormationDelete {
    fn run(&self, ctx: &mut Ctx) -> Result<()> {
        if ctx.args.fetch {
            let old_name = ctx.args.name_id.take();
            ctx.internal_run = true;
            SeaplaneFormationFetch.run(ctx)?;
            ctx.internal_run = false;
            ctx.args.name_id = old_name;
        }

        let formation_ctx = ctx.formation_ctx.get_or_init();

        if !formation_ctx.local && !formation_ctx.remote {
            cli_eprint!(@Red, "error: ");
            cli_eprintln!("nothing to do");
            cli_eprint!("(hint: either remove ");
            cli_eprint!(@Yellow, "--no-local ");
            cli_eprint!("or add ");
            cli_eprint!(@Yellow, "--remote ");
            cli_eprintln!("to the command)");
            std::process::exit(1);
        }

        // Get the indices of any formations that match the given name/ID
        let indices = if ctx.args.all {
            ctx.db
                .formations
                .formation_indices_of_left_matches(&formation_ctx.name_id)
        } else {
            ctx.db
                .formations
                .formation_indices_of_matches(&formation_ctx.name_id)
        };

        match indices.len() {
            0 => errors::no_matching_item(formation_ctx.name_id.clone(), false, ctx.args.all)?,
            1 => (),
            _ => {
                if !(ctx.args.all || ctx.args.force) {
                    errors::ambiguous_item(formation_ctx.name_id.clone(), true)?;
                }
            }
        }

        let mut deleted = indices.len();

        // Remove the Formations
        //
        // First try to delete the remote formation if required, because we don't want to delete
        // the local one too if this fails
        if formation_ctx.remote {
            let mut req = FormationsReq::new_delay_token(ctx)?;
            for idx in &indices {
                let formation = ctx.db.formations.get_formation(*idx).unwrap();
                if let Some(name) = &formation.name {
                    req.set_name(name)?;
                    let cfg_uuids = match req.delete(ctx.args.force) {
                        Err(e) => {
                            if matches!(
                                e.kind(),
                                CliErrorKind::Seaplane(SeaplaneError::ApiResponse(ae))
                                if ae.kind == ApiErrorKind::NotFound)
                            {
                                continue;
                            }
                            return Err(e);
                        }
                        Ok(cfg_uuids) => cfg_uuids,
                    };
                    cli_print!("Deleted remote Formation Instance '");
                    cli_print!(@Green, "{}", name);
                    if cfg_uuids.is_empty() {
                        cli_println!("'");
                    } else {
                        cli_println!("' with Configuration UUIDs:");
                        for uuid in cfg_uuids.into_iter() {
                            cli_println!(@Green, "\t{uuid}");
                        }
                    }
                } else {
                    return Err(CliErrorKind::NoMatchingItem(formation_ctx.name_id.clone())
                        .into_err()
                        .context("(hint: create the local Formation Plan with '")
                        .color_context(Color::Green, "seaplane formation plan")
                        .context("')\n")
                        .context("(hint: or try synchronizing local Plan definitions with remote instances by using '")
                        .color_context(Color::Green, "seaplane formation fetch-remote")
                        .context("')\n")
                        .context("(hint: You can also fetch remote instances by adding 'seaplane formation delete ")
                        .color_context(Color::Green, "--fetch")
                        .context("')\n"));
                }
            }
        }
        if formation_ctx.local && !ctx.args.stateless {
            // No need to potentially clone over and over
            let mut cloned_ctx = ctx.clone();
            for formation in ctx.db.formations.remove_formation_indices(&indices).iter() {
                let ids = if ctx.args.force {
                    formation.local.iter().cloned().collect()
                } else {
                    formation.local_only_configs()
                };
                for id in ids {
                    if let Some(cfg) = ctx.db.formations.remove_configuration(&id) {
                        if formation_ctx.recursive {
                            cloned_ctx.internal_run = true;
                            for flight in cfg.model.flights() {
                                let flight_name = flight.name();
                                if !ctx
                                    .db
                                    .formations
                                    .configurations()
                                    .filter(|cfg| cfg.id != id)
                                    .any(|cfg| {
                                        cfg.model.flights().iter().any(|f| f.name() == flight_name)
                                    })
                                {
                                    cloned_ctx.args.name_id = Some(flight_name.to_string());
                                    SeaplaneFlightDelete.run(&mut cloned_ctx)?;
                                    deleted += 1;
                                }
                            }
                        }
                    }
                }
                cli_println!("Deleted local Formation Plan {}", &formation.id.to_string());
            }
        }

        ctx.persist_formations()?;

        // TODO: recalculate dichotomy of local v. remote numbers (i.e. --no-local, etc.)
        cli_println!(
            "\nSuccessfully removed {} item{}",
            deleted,
            if deleted > 1 { "s" } else { "" }
        );

        Ok(())
    }

    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
        ctx.args.force = matches.contains_id("force");
        ctx.args.all = matches.contains_id("all");
        ctx.args.fetch = matches.contains_id("fetch");
        let mut fctx = ctx.formation_ctx.get_mut_or_init();
        fctx.name_id = matches.get_one::<String>("formation").unwrap().to_string();
        fctx.remote = !matches.contains_id("no-remote");
        fctx.local = !matches.contains_id("no-local");
        fctx.recursive = matches.contains_id("recursive");

        Ok(())
    }
}