// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24

package system

import (
	"bytes"
	"context"
	"fmt"
	"strings"

	"github.com/containerd/errdefs"
	"github.com/docker/cli/cli"
	"github.com/docker/cli/cli/command"
	"github.com/docker/cli/cli/command/completion"
	"github.com/docker/cli/cli/command/inspect"
	flagsHelper "github.com/docker/cli/cli/flags"
	"github.com/moby/moby/api/types/image"
	"github.com/moby/moby/client"
	"github.com/spf13/cobra"
)

type objectType = string

const (
	typeConfig    objectType = "config"
	typeContainer objectType = "container"
	typeImage     objectType = "image"
	typeNetwork   objectType = "network"
	typeNode      objectType = "node"
	typePlugin    objectType = "plugin"
	typeSecret    objectType = "secret"
	typeService   objectType = "service"
	typeTask      objectType = "task"
	typeVolume    objectType = "volume"
)

var allTypes = []objectType{
	typeConfig,
	typeContainer,
	typeImage,
	typeNetwork,
	typeNode,
	typePlugin,
	typeSecret,
	typeService,
	typeTask,
	typeVolume,
}

type inspectOptions struct {
	format     string
	objectType objectType
	size       bool
	ids        []string
}

// newInspectCommand creates a new cobra.Command for `docker inspect`
func newInspectCommand(dockerCLI command.Cli) *cobra.Command {
	var opts inspectOptions

	cmd := &cobra.Command{
		Use:   "inspect [OPTIONS] NAME|ID [NAME|ID...]",
		Short: "Return low-level information on Docker objects",
		Args:  cli.RequiresMinArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.ids = args
			if cmd.Flags().Changed("type") && opts.objectType == "" {
				return fmt.Errorf(`type is empty: must be one of "%s"`, strings.Join(allTypes, `", "`))
			}
			return runInspect(cmd.Context(), dockerCLI, opts)
		},
		// TODO(thaJeztah): should we consider adding completion for common object-types? (images, containers?)
		ValidArgsFunction:     completeObjectNames(dockerCLI),
		DisableFlagsInUseLine: true,
	}

	flags := cmd.Flags()
	flags.StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
	flags.StringVar(&opts.objectType, "type", "", "Only inspect objects of the given type")
	flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes if the type is container")

	_ = cmd.RegisterFlagCompletionFunc("type", completion.FromList(allTypes...))

	return cmd
}

func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
	var elementSearcher inspect.GetRefFunc
	switch opts.objectType {
	case "", typeConfig, typeContainer, typeImage, typeNetwork, typeNode,
		typePlugin, typeSecret, typeService, typeTask, typeVolume:
		elementSearcher = inspectAll(ctx, dockerCli, opts.size, opts.objectType)
	default:
		return fmt.Errorf(`unknown type: %q: must be one of "%s"`, opts.objectType, strings.Join(allTypes, `", "`))
	}
	return inspect.Inspect(dockerCli.Out(), opts.ids, opts.format, elementSearcher)
}

func inspectContainers(ctx context.Context, dockerCli command.Cli, getSize bool) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().ContainerInspect(ctx, ref, client.ContainerInspectOptions{Size: getSize})
		if err != nil {
			return nil, nil, err
		}
		return res.Container, res.Raw, err
	}
}

func inspectImages(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		var buf bytes.Buffer
		resp, err := dockerCli.Client().ImageInspect(ctx, ref, client.ImageInspectWithRawResponse(&buf))
		if err != nil {
			return image.InspectResponse{}, nil, err
		}
		return resp, buf.Bytes(), err
	}
}

func inspectNetwork(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().NetworkInspect(ctx, ref, client.NetworkInspectOptions{})
		return res.Network, res.Raw, err
	}
}

func inspectNode(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().NodeInspect(ctx, ref, client.NodeInspectOptions{})
		return res.Node, res.Raw, err
	}
}

func inspectService(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		// Service inspect shows defaults values in empty fields.
		res, err := dockerCli.Client().ServiceInspect(ctx, ref, client.ServiceInspectOptions{InsertDefaults: true})
		return res.Service, res.Raw, err
	}
}

func inspectTasks(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().TaskInspect(ctx, ref, client.TaskInspectOptions{})
		return res.Task, res.Raw, err
	}
}

func inspectVolume(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().VolumeInspect(ctx, ref, client.VolumeInspectOptions{})
		return res.Volume, res.Raw, err
	}
}

func inspectPlugin(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().PluginInspect(ctx, ref, client.PluginInspectOptions{})
		return res.Plugin, res.Raw, err
	}
}

func inspectSecret(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCli.Client().SecretInspect(ctx, ref, client.SecretInspectOptions{})
		return res.Secret, res.Raw, err
	}
}

func inspectConfig(ctx context.Context, dockerCLI command.Cli) inspect.GetRefFunc {
	return func(ref string) (any, []byte, error) {
		res, err := dockerCLI.Client().ConfigInspect(ctx, ref, client.ConfigInspectOptions{})
		return res.Config, res.Raw, err
	}
}

func inspectAll(ctx context.Context, dockerCLI command.Cli, getSize bool, typeConstraint objectType) inspect.GetRefFunc {
	inspectAutodetect := []struct {
		objectType      objectType
		isSizeSupported bool
		isSwarmObject   bool
		objectInspector func(string) (any, []byte, error)
	}{
		{
			objectType:      typeContainer,
			isSizeSupported: true,
			objectInspector: inspectContainers(ctx, dockerCLI, getSize),
		},
		{
			objectType:      typeImage,
			objectInspector: inspectImages(ctx, dockerCLI),
		},
		{
			objectType:      typeNetwork,
			objectInspector: inspectNetwork(ctx, dockerCLI),
		},
		{
			objectType:      typeVolume,
			objectInspector: inspectVolume(ctx, dockerCLI),
		},
		{
			objectType:      typeService,
			isSwarmObject:   true,
			objectInspector: inspectService(ctx, dockerCLI),
		},
		{
			objectType:      typeTask,
			isSwarmObject:   true,
			objectInspector: inspectTasks(ctx, dockerCLI),
		},
		{
			objectType:      typeNode,
			isSwarmObject:   true,
			objectInspector: inspectNode(ctx, dockerCLI),
		},
		{
			objectType:      typePlugin,
			objectInspector: inspectPlugin(ctx, dockerCLI),
		},
		{
			objectType:      typeSecret,
			isSwarmObject:   true,
			objectInspector: inspectSecret(ctx, dockerCLI),
		},
		{
			objectType:      typeConfig,
			isSwarmObject:   true,
			objectInspector: inspectConfig(ctx, dockerCLI),
		},
	}

	// isSwarmManager does an Info API call to verify that the daemon is
	// a swarm manager.
	isSwarmManager := func() bool {
		res, err := dockerCLI.Client().Info(ctx, client.InfoOptions{})
		if err != nil {
			_, _ = fmt.Fprintln(dockerCLI.Err(), err)
			return false
		}
		return res.Info.Swarm.ControlAvailable
	}

	return func(ref string) (any, []byte, error) {
		const (
			swarmSupportUnknown = iota
			swarmSupported
			swarmUnsupported
		)

		isSwarmSupported := swarmSupportUnknown

		for _, inspectData := range inspectAutodetect {
			if typeConstraint != "" && inspectData.objectType != typeConstraint {
				continue
			}
			if typeConstraint == "" && inspectData.isSwarmObject {
				if isSwarmSupported == swarmSupportUnknown {
					if isSwarmManager() {
						isSwarmSupported = swarmSupported
					} else {
						isSwarmSupported = swarmUnsupported
					}
				}
				if isSwarmSupported == swarmUnsupported {
					continue
				}
			}
			v, raw, err := inspectData.objectInspector(ref)
			if err != nil {
				if typeConstraint == "" && isErrSkippable(err) {
					continue
				}
				return v, raw, err
			}
			if getSize && !inspectData.isSizeSupported {
				_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING: --size ignored for", inspectData.objectType)
			}
			return v, raw, err
		}
		return nil, nil, fmt.Errorf("error: no such object: %s", ref)
	}
}

func isErrSkippable(err error) bool {
	return errdefs.IsNotFound(err) ||
		strings.Contains(err.Error(), "not supported") ||
		strings.Contains(err.Error(), "invalid reference format")
}
