package inspect

import (
	"bytes"
	"context"
	"encoding/json" // due to a bug in json-iterator it cannot be used here
	"fmt"
	"os"
	"regexp"
	"strings"
	"text/tabwriter"
	"text/template"

	"github.com/containers/common/pkg/completion"
	"github.com/containers/common/pkg/report"
	"github.com/containers/podman/v3/cmd/podman/common"
	"github.com/containers/podman/v3/cmd/podman/registry"
	"github.com/containers/podman/v3/cmd/podman/validate"
	"github.com/containers/podman/v3/libpod/define"
	"github.com/containers/podman/v3/pkg/domain/entities"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

const (
	// AllType can be of type ImageType or ContainerType.
	AllType = "all"
	// ContainerType is the container type.
	ContainerType = "container"
	// ImageType is the image type.
	ImageType = "image"
	// NetworkType is the network type
	NetworkType = "network"
	// PodType is the pod type.
	PodType = "pod"
	// VolumeType is the volume type
	VolumeType = "volume"
)

// AddInspectFlagSet takes a command and adds the inspect flags and returns an
// InspectOptions object.
func AddInspectFlagSet(cmd *cobra.Command) *entities.InspectOptions {
	opts := entities.InspectOptions{}

	flags := cmd.Flags()
	flags.BoolVarP(&opts.Size, "size", "s", false, "Display total file size")

	formatFlagName := "format"
	flags.StringVarP(&opts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
	_ = cmd.RegisterFlagCompletionFunc(formatFlagName, completion.AutocompleteNone)

	typeFlagName := "type"
	flags.StringVarP(&opts.Type, typeFlagName, "t", AllType, fmt.Sprintf("Specify inspect-object type (%q, %q or %q)", ImageType, ContainerType, AllType))
	_ = cmd.RegisterFlagCompletionFunc(typeFlagName, common.AutocompleteInspectType)

	validate.AddLatestFlag(cmd, &opts.Latest)
	return &opts
}

// Inspect inspects the specified container/image names or IDs.
func Inspect(namesOrIDs []string, options entities.InspectOptions) error {
	inspector, err := newInspector(options)
	if err != nil {
		return err
	}
	return inspector.inspect(namesOrIDs)
}

// inspector allows for inspecting images and containers.
type inspector struct {
	containerEngine entities.ContainerEngine
	imageEngine     entities.ImageEngine
	options         entities.InspectOptions
	podOptions      entities.PodInspectOptions
}

// newInspector creates a new inspector based on the specified options.
func newInspector(options entities.InspectOptions) (*inspector, error) {
	switch options.Type {
	case ImageType, ContainerType, AllType, PodType, NetworkType, VolumeType:
		// Valid types.
	default:
		return nil, errors.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, or %q", options.Type, ImageType, ContainerType, PodType, NetworkType, VolumeType, AllType)
	}
	if options.Type == ImageType {
		if options.Latest {
			return nil, errors.Errorf("latest is not supported for type %q", ImageType)
		}
		if options.Size {
			return nil, errors.Errorf("size is not supported for type %q", ImageType)
		}
	}
	if options.Type == PodType && options.Size {
		return nil, errors.Errorf("size is not supported for type %q", PodType)
	}
	podOpts := entities.PodInspectOptions{
		Latest: options.Latest,
		Format: options.Format,
	}
	return &inspector{
		containerEngine: registry.ContainerEngine(),
		imageEngine:     registry.ImageEngine(),
		options:         options,
		podOptions:      podOpts,
	}, nil
}

// inspect inspects the specified container/image names or IDs.
func (i *inspector) inspect(namesOrIDs []string) error {
	// data - dumping place for inspection results.
	var data []interface{} // nolint
	var errs []error
	ctx := context.Background()

	if len(namesOrIDs) == 0 {
		if !i.options.Latest && !i.options.All {
			return errors.New("no names or ids specified")
		}
	}

	tmpType := i.options.Type
	if i.options.Latest {
		if len(namesOrIDs) > 0 {
			return errors.New("--latest and arguments cannot be used together")
		}
		if i.options.Type == AllType {
			tmpType = ContainerType // -l works with --type=all, defaults to containertype
		}
	}

	// Inspect - note that AllType requires us to expensively query one-by-one.
	switch tmpType {
	case AllType:
		allData, allErrs, err := i.inspectAll(ctx, namesOrIDs)
		if err != nil {
			return err
		}
		data = allData
		errs = allErrs
	case ImageType:
		imgData, allErrs, err := i.imageEngine.Inspect(ctx, namesOrIDs, i.options)
		if err != nil {
			return err
		}
		errs = allErrs
		for i := range imgData {
			data = append(data, imgData[i])
		}
	case ContainerType:
		ctrData, allErrs, err := i.containerEngine.ContainerInspect(ctx, namesOrIDs, i.options)
		if err != nil {
			return err
		}
		errs = allErrs
		for i := range ctrData {
			data = append(data, ctrData[i])
		}
	case PodType:
		for _, pod := range namesOrIDs {
			i.podOptions.NameOrID = pod
			podData, err := i.containerEngine.PodInspect(ctx, i.podOptions)
			if err != nil {
				cause := errors.Cause(err)
				if !strings.Contains(cause.Error(), define.ErrNoSuchPod.Error()) {
					errs = []error{err}
				} else {
					return err
				}
			} else {
				errs = nil
				data = append(data, podData)
			}
		}
		if i.podOptions.Latest { // latest means there are no names in the namesOrID array
			podData, err := i.containerEngine.PodInspect(ctx, i.podOptions)
			if err != nil {
				cause := errors.Cause(err)
				if !strings.Contains(cause.Error(), define.ErrNoSuchPod.Error()) {
					errs = []error{err}
				} else {
					return err
				}
			} else {
				errs = nil
				data = append(data, podData)
			}
		}
	case NetworkType:
		networkData, allErrs, err := registry.ContainerEngine().NetworkInspect(ctx, namesOrIDs, i.options)
		if err != nil {
			return err
		}
		errs = allErrs
		for i := range networkData {
			data = append(data, networkData[i])
		}
	case VolumeType:
		volumeData, allErrs, err := i.containerEngine.VolumeInspect(ctx, namesOrIDs, i.options)
		if err != nil {
			return err
		}
		errs = allErrs
		for i := range volumeData {
			data = append(data, volumeData[i])
		}
	default:
		return errors.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, or %q", i.options.Type, ImageType, ContainerType, PodType, NetworkType, VolumeType, AllType)
	}
	// Always print an empty array
	if data == nil {
		data = []interface{}{}
	}

	var err error
	switch {
	case report.IsJSON(i.options.Format) || i.options.Format == "":
		err = printJSON(data)
	default:
		row := inspectNormalize(i.options.Format)
		row = "{{range . }}" + report.NormalizeFormat(row) + "{{end}}"
		err = printTmpl(tmpType, row, data)
	}
	if err != nil {
		logrus.Errorf("Error printing inspect output: %v", err)
	}

	if len(errs) > 0 {
		if len(errs) > 1 {
			for _, err := range errs[1:] {
				fmt.Fprintf(os.Stderr, "error inspecting object: %v\n", err)
			}
		}
		return errors.Errorf("error inspecting object: %v", errs[0])
	}
	return nil
}

func printJSON(data []interface{}) error {
	buf, err := json.MarshalIndent(data, "", "    ")
	if err != nil {
		return err
	}
	_, err = fmt.Println(string(buf))
	return err
}

func printTmpl(typ, row string, data []interface{}) error {
	t, err := template.New(typ + " inspect").Funcs(map[string]interface{}{
		"json": func(v interface{}) string {
			b := &bytes.Buffer{}
			e := registry.JSONLibrary().NewEncoder(b)
			e.SetEscapeHTML(false)
			_ = e.Encode(v)
			return strings.TrimSpace(b.String())
		},
	}).Parse(row)
	if err != nil {
		return err
	}
	w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0)
	return t.Execute(w, data)
}

func (i *inspector) inspectAll(ctx context.Context, namesOrIDs []string) ([]interface{}, []error, error) {
	var data []interface{} // nolint
	allErrs := []error{}
	for _, name := range namesOrIDs {
		ctrData, errs, err := i.containerEngine.ContainerInspect(ctx, []string{name}, i.options)
		if err != nil {
			return nil, nil, err
		}
		if len(errs) == 0 {
			data = append(data, ctrData[0])
			continue
		}
		imgData, errs, err := i.imageEngine.Inspect(ctx, []string{name}, i.options)
		if err != nil {
			return nil, nil, err
		}
		if len(errs) == 0 {
			data = append(data, imgData[0])
			continue
		}
		volumeData, errs, err := i.containerEngine.VolumeInspect(ctx, []string{name}, i.options)
		if err != nil {
			return nil, nil, err
		}
		if len(errs) == 0 {
			data = append(data, volumeData[0])
			continue
		}
		networkData, errs, err := registry.ContainerEngine().NetworkInspect(ctx, namesOrIDs, i.options)
		if err != nil {
			return nil, nil, err
		}
		if len(errs) == 0 {
			data = append(data, networkData[0])
			continue
		}
		i.podOptions.NameOrID = name
		podData, err := i.containerEngine.PodInspect(ctx, i.podOptions)
		if err != nil {
			cause := errors.Cause(err)
			if !strings.Contains(cause.Error(), define.ErrNoSuchPod.Error()) {
				return nil, nil, err
			}
		} else {
			data = append(data, podData)
			continue
		}
		if len(errs) > 0 {
			allErrs = append(allErrs, errors.Errorf("no such object: %q", name))
			continue
		}
	}
	return data, allErrs, nil
}

func inspectNormalize(row string) string {
	m := regexp.MustCompile(`{{\s*\.Id\s*}}`)
	row = m.ReplaceAllString(row, "{{.ID}}")

	r := strings.NewReplacer(
		".Src", ".Source",
		".Dst", ".Destination",
		".ImageID", ".Image",
	)
	return r.Replace(row)
}
