// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

// Package cli handles all of the core command line parsing. It's the first
// entry point after the real main function, and it imports and runs our core
// "lib".
package cli

import (
	"context"
	"fmt"
	"os"

	cliUtil "github.com/purpleidea/mgmt/cli/util"
	"github.com/purpleidea/mgmt/util/errwrap"

	"github.com/alexflint/go-arg"
)

func init() {
	if _, err := arg.NewParser(arg.Config{}, &Args{}); err != nil { // sanity check
		panic(errwrap.Wrapf(err, "invalid args cli struct"))
	}
}

// CLI is the entry point for using mgmt normally from the CLI.
func CLI(ctx context.Context, data *cliUtil.Data) (reterr error) {
	// Log any error we're about to return so that we don't fail silently.
	// Without this, the top-level main would only set the exit code.
	defer func() {
		if reterr != nil && data != nil && data.Flags.Logf != nil {
			data.Flags.Logf("error: %v", reterr)
		}
	}()
	// test for sanity
	if data == nil {
		return fmt.Errorf("this CLI was not run correctly")
	}
	if data.Program == "" || data.Version == "" {
		return fmt.Errorf("program was not compiled correctly")
	}
	if data.Copying == "" {
		return fmt.Errorf("program copyrights were removed, can't run")
	}

	args := Args{}
	args.version = data.Version // copy this in
	args.description = data.Tagline

	config := arg.Config{
		Program: data.Program,
	}
	parser, err := arg.NewParser(config, &args)
	if err != nil {
		// programming error
		return errwrap.Wrapf(err, "cli config error")
	}
	err = parser.Parse(NormalizeArgs(data.Args[1:])) // XXX: args[0] needs to be dropped
	if err == arg.ErrHelp {
		parser.WriteHelp(os.Stdout)
		return nil
	}
	if err == arg.ErrVersion {
		fmt.Printf("%s\n", data.Version) // byon: bring your own newline
		return nil
	}
	if err != nil {
		//parser.WriteHelp(os.Stdout) // TODO: is doing this helpful?
		return cliUtil.CliParseError(err) // consistent errors
	}

	// display the license
	if args.License {
		fmt.Printf("%s", data.Copying) // file comes with a trailing nl
		return nil
	}

	if ok, err := args.Run(ctx, data); err != nil {
		return err
	} else if ok { // did we activate one of the commands?
		return nil
	}

	// print help if no subcommands are set
	parser.WriteHelp(os.Stdout)

	return nil
}

// Args is the CLI parsing structure and type of the parsed result. This
// particular struct is the top-most one.
type Args struct {
	// XXX: We cannot have both subcommands and a positional argument.
	// XXX: I think it's a bug of this library that it can't handle argv[0].
	//Argv0 string `arg:"positional"`

	License bool `arg:"--license" help:"display the license and exit"`

	HelpCmd *HelpArgs `arg:"subcommand:help" help:"display this help and exit"`

	FmtCmd *FmtArgs `arg:"subcommand:fmt" help:"format mcl code"`

	CheckCmd *CheckArgs `arg:"subcommand:check" help:"check code on this machine"`

	RunCmd *RunArgs `arg:"subcommand:run" help:"run code on this machine"`

	DeployCmd *DeployArgs `arg:"subcommand:deploy" help:"deploy code into a cluster"`

	SetupCmd *SetupArgs `arg:"subcommand:setup" help:"setup some bootstrapping tasks"`

	FirstbootCmd *FirstbootArgs `arg:"subcommand:firstboot" help:"run some tasks on first boot"`

	DocsCmd *DocsGenerateArgs `arg:"subcommand:docs" help:"generate documentation"`

	ToolsCmd *ToolsArgs `arg:"subcommand:tools" help:"collection of useful tools"`

	// This never runs, it gets preempted in the real main() function.
	// XXX: Can we do it nicely with the new arg parser? can it ignore all
	// args?
	EtcdCmd *EtcdArgs `arg:"subcommand:etcd" help:"run standalone etcd"`

	// This never runs, it gets preempted in the real main() function.
	ProvisionerCmd *ProvisionerArgs `arg:"subcommand:provisioner" help:"provision bare metal machines"`

	// version is a private handle for our version string.
	version string `arg:"-"` // ignored from parsing

	// description is a private handle for our description string.
	description string `arg:"-"` // ignored from parsing
}

// Version returns the version string. Implementing this signature is part of
// the API for the cli library.
func (obj *Args) Version() string {
	return obj.version
}

// Description returns a description string. Implementing this signature is part
// of the API for the cli library.
func (obj *Args) Description() string {
	return obj.description
}

// Run executes the correct subcommand. It errors if there's ever an error. It
// returns true if we did activate one of the subcommands. It returns false if
// we did not. This information is used so that the top-level parser can return
// usage or help information if no subcommand activates.
func (obj *Args) Run(ctx context.Context, data *cliUtil.Data) (bool, error) {
	if cmd := obj.HelpCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.FmtCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.CheckCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.RunCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.DeployCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.SetupCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.FirstbootCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.DocsCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	if cmd := obj.ToolsCmd; cmd != nil {
		return cmd.Run(ctx, data)
	}

	// NOTE: we could return true, fmt.Errorf("...") if more than one did
	return false, nil // nobody activated
}

// EtcdArgs is the CLI parsing structure and type of the parsed result. This
// particular one is empty because the `etcd` subcommand is preempted in the
// real main() function.
type EtcdArgs struct{}

// ProvisionerArgs is the CLI parsing structure and type of the parsed result.
// This particular one is empty because the `provisioner` subcommand is
// preempted in the real main() function via the entry package.
type ProvisionerArgs struct{}
