package cli

import (
	"context"
	"fmt"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/status"

	"github.com/hashicorp/vagrant-plugin-sdk/component"
	"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
	"github.com/hashicorp/vagrant-plugin-sdk/terminal"
	"github.com/hashicorp/vagrant/internal/client"
	"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)

type DynamicCommand struct {
	*baseCommand

	name     string
	synopsis string
	help     string
	parent   *DynamicCommand
	flags    []*component.CommandFlag
	primary  bool
}

func (c *DynamicCommand) Run(args []string) int {
	if err := c.Init(
		WithArgs(args),
		WithFlags(c.Flags()),
	); err != nil {
		return 1
	}

	var r *vagrant_server.Job_RunResult
	err := c.Do(c.Ctx, func(ctx context.Context, cl *client.Client, modifier client.JobModifier) (err error) {
		taskArgs := &vagrant_plugin_sdk.Command_Arguments{
			Args:  c.args,
			Flags: []*vagrant_plugin_sdk.Command_Arguments_Flag{},
		}
		for f, v := range c.flagData {
			cmdFlag := &vagrant_plugin_sdk.Command_Arguments_Flag{Name: f.LongName}
			switch f.Type {
			case component.FlagBool:
				cmdFlag.Type = vagrant_plugin_sdk.Command_Arguments_Flag_BOOL
				cmdFlag.Value = &vagrant_plugin_sdk.Command_Arguments_Flag_Bool{
					Bool: v.(bool),
				}
			case component.FlagString:
				cmdFlag.Type = vagrant_plugin_sdk.Command_Arguments_Flag_STRING
				cmdFlag.Value = &vagrant_plugin_sdk.Command_Arguments_Flag_String_{
					String_: v.(string),
				}
			}
			taskArgs.Flags = append(taskArgs.Flags, cmdFlag)
		}

		c.Log.Debug("collected argument flags",
			"flags", taskArgs.Flags,
			"args", args,
			"remaining", c.args,
		)

		t := &vagrant_server.Task{
			Task: c.name,
			Component: &vagrant_server.Component{
				Type: vagrant_server.Component_COMMAND,
				Name: c.name,
			},
			CliArgs:     taskArgs,
			CommandName: c.name,
		}

		if c.basis != nil {
			t.Scope = &vagrant_server.Task_Basis{
				Basis: c.basis.Ref(),
			}
		}

		if c.project != nil {
			t.Scope = &vagrant_server.Task_Project{
				Project: c.project.Ref(),
			}
		}

		if c.target != nil {
			t.Scope = &vagrant_server.Task_Target{
				Target: c.target.Ref(),
			}
		}

		r, err = cl.Task(ctx,
			&vagrant_server.Job_RunOp{Task: t},
			modifier,
		)

		// If nothing failed but we didn't get a Result back, something may
		// have gone wrong on the far side so we need to interpret the error.
		if err == nil && !r.RunResult {
			runErrorStatus := status.FromProto(r.RunError)
			details := runErrorStatus.Details()
			userError := false
			for _, msg := range details {
				switch m := msg.(type) {
				case *errdetails.LocalizedMessage:
					// Errors from Ruby with LocalizedMessages are user-facing,
					// so can be output directly.
					userError = true
					cl.UI().Output(m.Message, terminal.WithErrorStyle())
					// All user-facing errors from Ruby use a 1 exit code. See
					// Vagrant::Errors::VagrantError.
					r.ExitCode = 1
				}
			}
			// If there wasn't a user-facing error, just assign the returned
			// error (if any) from the response and assign that back out so it
			// can be displayed as an unexpected error.
			if !userError {
				err = runErrorStatus.Err()
			}
		}

		if err != nil {
			cl.UI().Output("Running of task "+c.name+" failed unexpectedly\n", terminal.WithErrorStyle())
			cl.UI().Output("Error: "+err.Error(), terminal.WithErrorStyle())
		}

		c.Log.Debug("result from operation", "task", c.name, "result", r)

		return err
	})

	if err != nil {
		c.Log.Error("Got error from task, so exiting 255", "error", err)
		return int(-1)
	}

	c.Log.Info("Task did not error, so exiting with provided code", "code", r.ExitCode)
	return int(r.ExitCode)
}

func (c *DynamicCommand) Synopsis() string {
	return c.synopsis
}

func (c *DynamicCommand) Help() string {
	fset := c.generateCliFlags(c.Flags())
	return formatHelp(fmt.Sprintf("%s\n%s\n", c.help, fset.Display()))
}

func (c *DynamicCommand) Flags() component.CommandFlags {
	return c.flagSet(flagSetOperation, func(opts []*component.CommandFlag) []*component.CommandFlag {
		return append(c.flags, opts...)
	})
}

func (c *DynamicCommand) Primary() bool {
	return c.primary
}

func (c *DynamicCommand) fullName() string {
	var v string
	if c.parent != nil {
		v = c.parent.fullName() + " "
	}
	return v + c.name
}
