package manager

import (
	"fmt"
	"net/url"
	"os"
	"strings"
	"sync"

	"github.com/docker/cli/cli/command"
	"github.com/spf13/cobra"
	"go.opentelemetry.io/otel/attribute"
)

const (
	// CommandAnnotationPlugin is added to every stub command added by
	// AddPluginCommandStubs with the value "true" and so can be
	// used to distinguish plugin stubs from regular commands.
	CommandAnnotationPlugin = "com.docker.cli.plugin"

	// CommandAnnotationPluginVendor is added to every stub command
	// added by AddPluginCommandStubs and contains the vendor of
	// that plugin.
	CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"

	// CommandAnnotationPluginVersion is added to every stub command
	// added by AddPluginCommandStubs and contains the version of
	// that plugin.
	CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"

	// CommandAnnotationPluginInvalid is added to any stub command
	// added by AddPluginCommandStubs for an invalid command (that
	// is, one which failed it's candidate test) and contains the
	// reason for the failure.
	CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"

	// CommandAnnotationPluginCommandPath is added to overwrite the
	// command path for a plugin invocation.
	CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)

var pluginCommandStubsOnce sync.Once

// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
	pluginCommandStubsOnce.Do(func() {
		var plugins []Plugin
		plugins, err = ListPlugins(dockerCli, rootCmd)
		if err != nil {
			return
		}
		for _, p := range plugins {
			p := p
			vendor := p.Vendor
			if vendor == "" {
				vendor = "unknown"
			}
			annotations := map[string]string{
				CommandAnnotationPlugin:        "true",
				CommandAnnotationPluginVendor:  vendor,
				CommandAnnotationPluginVersion: p.Version,
			}
			if p.Err != nil {
				annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
			}
			rootCmd.AddCommand(&cobra.Command{
				Use:                p.Name,
				Short:              p.ShortDescription,
				Run:                func(_ *cobra.Command, _ []string) {},
				Annotations:        annotations,
				DisableFlagParsing: true,
				RunE: func(cmd *cobra.Command, args []string) error {
					flags := rootCmd.PersistentFlags()
					flags.SetOutput(nil)
					perr := flags.Parse(args)
					if perr != nil {
						return err
					}
					if flags.Changed("help") {
						cmd.HelpFunc()(rootCmd, args)
						return nil
					}
					return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", cmd.Name())
				},
				ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
					// Delegate completion to plugin
					cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
					cargs = append(cargs, args...)
					cargs = append(cargs, toComplete)
					os.Args = cargs
					runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
					if runErr != nil {
						return nil, cobra.ShellCompDirectiveError
					}
					runErr = runCommand.Run()
					if runErr == nil {
						os.Exit(0) // plugin already rendered complete data
					}
					return nil, cobra.ShellCompDirectiveError
				},
			})
		}
	})
	return err
}

const (
	dockerCliAttributePrefix = attribute.Key("docker.cli")

	cobraCommandPath = attribute.Key("cobra.command_path")
)

func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
	commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
	if commandPath == "" {
		commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
	}

	attrSet := attribute.NewSet(
		cobraCommandPath.String(commandPath),
	)

	kvs := make([]attribute.KeyValue, 0, attrSet.Len())
	for iter := attrSet.Iter(); iter.Next(); {
		attr := iter.Attribute()
		kvs = append(kvs, attribute.KeyValue{
			Key:   dockerCliAttributePrefix + "." + attr.Key,
			Value: attr.Value,
		})
	}
	return attribute.NewSet(kvs...)
}

func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
	if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
		// values in environment variables need to be in baggage format
		// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
		attrsSlice := make([]string, attrs.Len())
		for iter := attrs.Iter(); iter.Next(); {
			i, v := iter.IndexedAttribute()
			attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
		}
		env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
	}
	return env
}