package varlinkapi

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/containers/buildah"
	"github.com/containers/buildah/imagebuildah"
	"github.com/containers/image/docker"
	dockerarchive "github.com/containers/image/docker/archive"
	"github.com/containers/image/manifest"
	"github.com/containers/image/transports/alltransports"
	"github.com/containers/image/types"
	"github.com/containers/libpod/cmd/podman/shared"
	"github.com/containers/libpod/cmd/podman/varlink"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/libpod/image"
	sysreg "github.com/containers/libpod/pkg/registries"
	"github.com/containers/libpod/pkg/util"
	"github.com/containers/libpod/utils"
	"github.com/containers/storage/pkg/archive"
	"github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// ListImages lists all the images in the store
// It requires no inputs.
func (i *LibpodAPI) ListImages(call iopodman.VarlinkCall) error {
	images, err := i.Runtime.ImageRuntime().GetImages()
	if err != nil {
		return call.ReplyErrorOccurred(fmt.Sprintf("unable to get list of images %q", err))
	}
	var imageList []iopodman.Image
	for _, image := range images {
		labels, _ := image.Labels(getContext())
		containers, _ := image.Containers()
		repoDigests, err := image.RepoDigests()
		if err != nil {
			return err
		}

		size, _ := image.Size(getContext())
		isParent, err := image.IsParent()
		if err != nil {
			return call.ReplyErrorOccurred(err.Error())
		}

		i := iopodman.Image{
			Id:          image.ID(),
			ParentId:    image.Parent,
			RepoTags:    image.Names(),
			RepoDigests: repoDigests,
			Created:     image.Created().Format(time.RFC3339),
			Size:        int64(*size),
			VirtualSize: image.VirtualSize,
			Containers:  int64(len(containers)),
			Labels:      labels,
			IsParent:    isParent,
		}
		imageList = append(imageList, i)
	}
	return call.ReplyListImages(imageList)
}

// GetImage returns a single image in the form of a Image
func (i *LibpodAPI) GetImage(call iopodman.VarlinkCall, id string) error {
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(id)
	if err != nil {
		return call.ReplyImageNotFound(id)
	}
	labels, err := newImage.Labels(getContext())
	if err != nil {
		return err
	}
	containers, err := newImage.Containers()
	if err != nil {
		return err
	}
	repoDigests, err := newImage.RepoDigests()
	if err != nil {
		return err
	}
	size, err := newImage.Size(getContext())
	if err != nil {
		return err
	}

	il := iopodman.Image{
		Id:          newImage.ID(),
		ParentId:    newImage.Parent,
		RepoTags:    newImage.Names(),
		RepoDigests: repoDigests,
		Created:     newImage.Created().Format(time.RFC3339),
		Size:        int64(*size),
		VirtualSize: newImage.VirtualSize,
		Containers:  int64(len(containers)),
		Labels:      labels,
	}
	return call.ReplyGetImage(il)
}

// BuildImage ...
func (i *LibpodAPI) BuildImage(call iopodman.VarlinkCall, config iopodman.BuildInfo) error {
	var (
		namespace []buildah.NamespaceOption
		err       error
	)

	systemContext := types.SystemContext{}
	contextDir := config.ContextDir

	newContextDir, err := ioutil.TempDir("", "buildTarball")
	if err != nil {
		call.ReplyErrorOccurred("unable to create tempdir")
	}
	logrus.Debugf("created new context dir at %s", newContextDir)

	reader, err := os.Open(contextDir)
	if err != nil {
		logrus.Errorf("failed to open the context dir tar file %s", contextDir)
		return call.ReplyErrorOccurred(fmt.Sprintf("unable to open context dir tar file %s", contextDir))
	}
	defer reader.Close()
	if err := archive.Untar(reader, newContextDir, &archive.TarOptions{}); err != nil {
		logrus.Errorf("fail to untar the context dir tarball (%s) to the context dir (%s)", contextDir, newContextDir)
		return call.ReplyErrorOccurred(fmt.Sprintf("unable to untar context dir %s", contextDir))
	}
	logrus.Debugf("untar of %s successful", contextDir)

	// All output (stdout, stderr) is captured in output as well
	output := bytes.NewBuffer([]byte{})

	commonOpts := &buildah.CommonBuildOptions{
		AddHost:      config.BuildOptions.AddHosts,
		CgroupParent: config.BuildOptions.CgroupParent,
		CPUPeriod:    uint64(config.BuildOptions.CpuPeriod),
		CPUQuota:     config.BuildOptions.CpuQuota,
		CPUSetCPUs:   config.BuildOptions.CpusetCpus,
		CPUSetMems:   config.BuildOptions.CpusetMems,
		Memory:       config.BuildOptions.Memory,
		MemorySwap:   config.BuildOptions.MemorySwap,
		ShmSize:      config.BuildOptions.ShmSize,
		Ulimit:       config.BuildOptions.Ulimit,
		Volumes:      config.BuildOptions.Volume,
	}

	hostNetwork := buildah.NamespaceOption{
		Name: specs.NetworkNamespace,
		Host: true,
	}

	namespace = append(namespace, hostNetwork)

	options := imagebuildah.BuildOptions{
		CommonBuildOpts:         commonOpts,
		AdditionalTags:          config.AdditionalTags,
		Annotations:             config.Annotations,
		Args:                    config.BuildArgs,
		CNIConfigDir:            config.CniConfigDir,
		CNIPluginPath:           config.CniPluginDir,
		Compression:             stringCompressionToArchiveType(config.Compression),
		ContextDirectory:        newContextDir,
		DefaultMountsFilePath:   config.DefaultsMountFilePath,
		Err:                     output,
		ForceRmIntermediateCtrs: config.ForceRmIntermediateCtrs,
		IIDFile:                 config.Iidfile,
		Labels:                  config.Label,
		Layers:                  config.Layers,
		NoCache:                 config.Nocache,
		Out:                     output,
		Output:                  config.Output,
		NamespaceOptions:        namespace,
		OutputFormat:            config.OutputFormat,
		PullPolicy:              stringPullPolicyToType(config.PullPolicy),
		Quiet:                   config.Quiet,
		RemoveIntermediateCtrs:  config.RemoteIntermediateCtrs,
		ReportWriter:            output,
		RuntimeArgs:             config.RuntimeArgs,
		SignaturePolicyPath:     config.SignaturePolicyPath,
		Squash:                  config.Squash,
		SystemContext:           &systemContext,
	}

	if call.WantsMore() {
		call.Continues = true
	}

	var newPathDockerFiles []string

	for _, d := range config.Dockerfiles {
		if strings.HasPrefix(d, "http://") ||
			strings.HasPrefix(d, "https://") ||
			strings.HasPrefix(d, "git://") ||
			strings.HasPrefix(d, "github.com/") {
			newPathDockerFiles = append(newPathDockerFiles, d)
			continue
		}
		base := filepath.Base(d)
		newPathDockerFiles = append(newPathDockerFiles, filepath.Join(newContextDir, base))
	}

	c := build(i.Runtime, options, newPathDockerFiles)
	var log []string
	done := false
	for {
		outputLine, err := output.ReadString('\n')
		if err == nil {
			log = append(log, outputLine)
			if call.WantsMore() {
				//	 we want to reply with what we have
				br := iopodman.MoreResponse{
					Logs: log,
				}
				call.ReplyBuildImage(br)
				log = []string{}
			}
			continue
		} else if err == io.EOF {
			select {
			case err := <-c:
				if err != nil {
					return call.ReplyErrorOccurred(err.Error())
				}
				done = true
			default:
				if call.WantsMore() {
					time.Sleep(1 * time.Second)
					break
				}
			}
		} else {
			return call.ReplyErrorOccurred(err.Error())
		}
		if done {
			break
		}
	}
	call.Continues = false

	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(config.Output)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	br := iopodman.MoreResponse{
		Logs: log,
		Id:   newImage.ID(),
	}
	return call.ReplyBuildImage(br)
}

func build(runtime *libpod.Runtime, options imagebuildah.BuildOptions, dockerfiles []string) chan error {
	c := make(chan error)
	go func() {
		err := runtime.Build(getContext(), options, dockerfiles...)
		c <- err
		close(c)
	}()

	return c
}

// InspectImage returns an image's inspect information as a string that can be serialized.
// Requires an image ID or name
func (i *LibpodAPI) InspectImage(call iopodman.VarlinkCall, name string) error {
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if err != nil {
		return call.ReplyImageNotFound(name)
	}
	inspectInfo, err := newImage.Inspect(getContext())
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	b, err := json.Marshal(inspectInfo)
	if err != nil {
		return call.ReplyErrorOccurred(fmt.Sprintf("unable to serialize"))
	}
	return call.ReplyInspectImage(string(b))
}

// HistoryImage returns the history of the image's layers
// Requires an image or name
func (i *LibpodAPI) HistoryImage(call iopodman.VarlinkCall, name string) error {
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if err != nil {
		return call.ReplyImageNotFound(name)
	}
	history, err := newImage.History(getContext())
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	var histories []iopodman.ImageHistory
	for _, hist := range history {
		imageHistory := iopodman.ImageHistory{
			Id:        hist.ID,
			Created:   hist.Created.Format(time.RFC3339),
			CreatedBy: hist.CreatedBy,
			Tags:      newImage.Names(),
			Size:      hist.Size,
			Comment:   hist.Comment,
		}
		histories = append(histories, imageHistory)
	}
	return call.ReplyHistoryImage(histories)
}

// PushImage pushes an local image to registry
func (i *LibpodAPI) PushImage(call iopodman.VarlinkCall, name, tag string, tlsVerify *bool, signaturePolicy, creds, certDir string, compress bool, format string, removeSignatures bool, signBy string) error {
	var (
		registryCreds *types.DockerAuthConfig
		manifestType  string
	)
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if err != nil {
		return call.ReplyImageNotFound(err.Error())
	}
	destname := name
	if tag != "" {
		destname = tag
	}
	if creds != "" {
		creds, err := util.ParseRegistryCreds(creds)
		if err != nil {
			return err
		}
		registryCreds = creds
	}
	dockerRegistryOptions := image.DockerRegistryOptions{
		DockerRegistryCreds: registryCreds,
		DockerCertPath:      certDir,
	}
	if tlsVerify != nil {
		dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!*tlsVerify)
	}
	if format != "" {
		switch format {
		case "oci": //nolint
			manifestType = v1.MediaTypeImageManifest
		case "v2s1":
			manifestType = manifest.DockerV2Schema1SignedMediaType
		case "v2s2", "docker":
			manifestType = manifest.DockerV2Schema2MediaType
		default:
			return call.ReplyErrorOccurred(fmt.Sprintf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", format))
		}
	}
	so := image.SigningOptions{
		RemoveSignatures: removeSignatures,
		SignBy:           signBy,
	}

	if call.WantsMore() {
		call.Continues = true
	}

	output := bytes.NewBuffer([]byte{})
	c := make(chan error)
	go func() {
		err := newImage.PushImageToHeuristicDestination(getContext(), destname, manifestType, "", signaturePolicy, output, compress, so, &dockerRegistryOptions, nil)
		c <- err
		close(c)
	}()

	// TODO When pull output gets fixed for the remote client, we need to look into how we can turn below
	// into something re-usable.  it is in build too
	var log []string
	done := false
	for {
		line, err := output.ReadString('\n')
		if err == nil {
			log = append(log, line)
			continue
		} else if err == io.EOF {
			select {
			case err := <-c:
				if err != nil {
					logrus.Errorf("reading of output during push failed for %s", newImage.ID())
					return call.ReplyErrorOccurred(err.Error())
				}
				done = true
			default:
				if !call.WantsMore() {
					time.Sleep(1 * time.Second)
					break
				}
				br := iopodman.MoreResponse{
					Logs: log,
				}
				call.ReplyPushImage(br)
				log = []string{}
			}
		} else {
			return call.ReplyErrorOccurred(err.Error())
		}
		if done {
			break
		}
	}
	call.Continues = false

	br := iopodman.MoreResponse{
		Logs: log,
	}
	return call.ReplyPushImage(br)
}

// TagImage accepts an image name and tag as strings and tags an image in the local store.
func (i *LibpodAPI) TagImage(call iopodman.VarlinkCall, name, tag string) error {
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if err != nil {
		return call.ReplyImageNotFound(name)
	}
	if err := newImage.TagImage(tag); err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyTagImage(newImage.ID())
}

// RemoveImage accepts a image name or ID as a string and force bool to determine if it should
// remove the image even if being used by stopped containers
func (i *LibpodAPI) RemoveImage(call iopodman.VarlinkCall, name string, force bool) error {
	ctx := getContext()
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if err != nil {
		return call.ReplyImageNotFound(name)
	}
	_, err = i.Runtime.RemoveImage(ctx, newImage, force)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyRemoveImage(newImage.ID())
}

// SearchImages searches all registries configured in /etc/containers/registries.conf for an image
// Requires an image name and a search limit as int
func (i *LibpodAPI) SearchImages(call iopodman.VarlinkCall, query string, limit *int64, tlsVerify *bool) error {
	sc := image.GetSystemContext("", "", false)
	if tlsVerify != nil {
		sc.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!*tlsVerify)
	}
	var registries []string

	// Check if search query has a registry in it
	registry, err := sysreg.GetRegistry(query)
	if err != nil {
		return call.ReplyErrorOccurred(fmt.Sprintf("error getting registry from %q: %q", query, err))
	}
	if registry != "" {
		registries = append(registries, registry)
		query = query[len(registry)+1:]
	} else {
		registries, err = sysreg.GetRegistries()
		if err != nil {
			return call.ReplyErrorOccurred(fmt.Sprintf("unable to get system registries: %q", err))
		}
	}
	var imageResults []iopodman.ImageSearchResult
	for _, reg := range registries {
		var lim = 1000
		if limit != nil {
			lim = int(*limit)
		}
		results, err := docker.SearchRegistry(getContext(), sc, reg, query, lim)
		if err != nil {
			// If we are searching multiple registries, don't make something like an
			// auth error fatal. Unfortunately we cannot differentiate between auth
			// errors and other possibles errors
			if len(registries) > 1 {
				continue
			}
			return call.ReplyErrorOccurred(err.Error())
		}
		for _, result := range results {
			i := iopodman.ImageSearchResult{
				Registry:     reg,
				Description:  result.Description,
				Is_official:  result.IsOfficial,
				Is_automated: result.IsAutomated,
				Name:         result.Name,
				Star_count:   int64(result.StarCount),
			}
			imageResults = append(imageResults, i)
		}
	}
	return call.ReplySearchImages(imageResults)
}

// DeleteUnusedImages deletes any images that do not have containers associated with it.
// TODO Filters are not implemented
func (i *LibpodAPI) DeleteUnusedImages(call iopodman.VarlinkCall) error {
	images, err := i.Runtime.ImageRuntime().GetImages()
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	var deletedImages []string
	for _, img := range images {
		containers, err := img.Containers()
		if err != nil {
			return call.ReplyErrorOccurred(err.Error())
		}
		if len(containers) == 0 {
			if err := img.Remove(false); err != nil {
				return call.ReplyErrorOccurred(err.Error())
			}
			deletedImages = append(deletedImages, img.ID())
		}
	}
	return call.ReplyDeleteUnusedImages(deletedImages)
}

// Commit ...
func (i *LibpodAPI) Commit(call iopodman.VarlinkCall, name, imageName string, changes []string, author, message string, pause bool, manifestType string) error {
	ctr, err := i.Runtime.LookupContainer(name)
	if err != nil {
		return call.ReplyContainerNotFound(name)
	}
	sc := image.GetSystemContext(i.Runtime.GetConfig().SignaturePolicyPath, "", false)
	var mimeType string
	switch manifestType {
	case "oci", "": //nolint
		mimeType = buildah.OCIv1ImageManifest
	case "docker":
		mimeType = manifest.DockerV2Schema2MediaType
	default:
		return call.ReplyErrorOccurred(fmt.Sprintf("unrecognized image format %q", manifestType))
	}
	coptions := buildah.CommitOptions{
		SignaturePolicyPath:   i.Runtime.GetConfig().SignaturePolicyPath,
		ReportWriter:          nil,
		SystemContext:         sc,
		PreferredManifestType: mimeType,
	}
	options := libpod.ContainerCommitOptions{
		CommitOptions: coptions,
		Pause:         pause,
		Message:       message,
		Changes:       changes,
		Author:        author,
	}

	newImage, err := ctr.Commit(getContext(), imageName, options)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyCommit(newImage.ID())
}

// ImportImage imports an image from a tarball to the image store
func (i *LibpodAPI) ImportImage(call iopodman.VarlinkCall, source, reference, message string, changes []string, delete bool) error {
	configChanges, err := util.GetImageConfig(changes)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	history := []v1.History{
		{Comment: message},
	}
	config := v1.Image{
		Config:  configChanges,
		History: history,
	}
	newImage, err := i.Runtime.ImageRuntime().Import(getContext(), source, reference, nil, image.SigningOptions{}, config)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	if delete {
		if err := os.Remove(source); err != nil {
			return call.ReplyErrorOccurred(err.Error())
		}
	}

	return call.ReplyImportImage(newImage.ID())
}

// ExportImage exports an image to the provided destination
// destination must have the transport type!!
func (i *LibpodAPI) ExportImage(call iopodman.VarlinkCall, name, destination string, compress bool, tags []string) error {
	newImage, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if err != nil {
		return call.ReplyImageNotFound(name)
	}

	additionalTags, err := image.GetAdditionalTags(tags)
	if err != nil {
		return err
	}

	if err := newImage.PushImageToHeuristicDestination(getContext(), destination, "", "", "", nil, compress, image.SigningOptions{}, &image.DockerRegistryOptions{}, additionalTags); err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyExportImage(newImage.ID())
}

// PullImage pulls an image from a registry to the image store.
func (i *LibpodAPI) PullImage(call iopodman.VarlinkCall, name string, certDir, creds, signaturePolicy string, tlsVerify *bool) error {
	var (
		registryCreds *types.DockerAuthConfig
		imageID       string
	)
	if creds != "" {
		creds, err := util.ParseRegistryCreds(creds)
		if err != nil {
			return err
		}
		registryCreds = creds
	}

	dockerRegistryOptions := image.DockerRegistryOptions{
		DockerRegistryCreds: registryCreds,
		DockerCertPath:      certDir,
	}
	if tlsVerify != nil {
		dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!*tlsVerify)
	}

	so := image.SigningOptions{}

	if strings.HasPrefix(name, dockerarchive.Transport.Name()+":") {
		srcRef, err := alltransports.ParseImageName(name)
		if err != nil {
			return errors.Wrapf(err, "error parsing %q", name)
		}
		newImage, err := i.Runtime.ImageRuntime().LoadFromArchiveReference(getContext(), srcRef, signaturePolicy, nil)
		if err != nil {
			return errors.Wrapf(err, "error pulling image from %q", name)
		}
		imageID = newImage[0].ID()
	} else {
		newImage, err := i.Runtime.ImageRuntime().New(getContext(), name, signaturePolicy, "", nil, &dockerRegistryOptions, so, false, nil)
		if err != nil {
			return call.ReplyErrorOccurred(fmt.Sprintf("unable to pull %s: %s", name, err.Error()))
		}
		imageID = newImage.ID()
	}
	return call.ReplyPullImage(imageID)
}

// ImageExists returns bool as to whether the input image exists in local storage
func (i *LibpodAPI) ImageExists(call iopodman.VarlinkCall, name string) error {
	_, err := i.Runtime.ImageRuntime().NewFromLocal(name)
	if errors.Cause(err) == image.ErrNoSuchImage {
		return call.ReplyImageExists(1)
	}
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyImageExists(0)
}

// ContainerRunlabel ...
func (i *LibpodAPI) ContainerRunlabel(call iopodman.VarlinkCall, input iopodman.Runlabel) error {
	ctx := getContext()
	dockerRegistryOptions := image.DockerRegistryOptions{
		DockerCertPath: input.CertDir,
	}
	if input.TlsVerify != nil {
		dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!*input.TlsVerify)
	}

	stdErr := os.Stderr
	stdOut := os.Stdout
	stdIn := os.Stdin

	runLabel, imageName, err := shared.GetRunlabel(input.Label, input.Image, ctx, i.Runtime, input.Pull, input.Creds, dockerRegistryOptions, input.Authfile, input.SignaturePolicyPath, nil)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	if runLabel == "" {
		return call.ReplyErrorOccurred(fmt.Sprintf("%s does not contain the label %s", input.Image, input.Label))
	}

	cmd, env, err := shared.GenerateRunlabelCommand(runLabel, imageName, input.Name, input.Opts, input.ExtraArgs)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	if err := utils.ExecCmdWithStdStreams(stdIn, stdOut, stdErr, env, cmd[0], cmd[1:]...); err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyContainerRunlabel()
}

// ImagesPrune ....
func (i *LibpodAPI) ImagesPrune(call iopodman.VarlinkCall, all bool) error {
	prunedImages, err := i.Runtime.ImageRuntime().PruneImages(all)
	if err != nil {
		return call.ReplyErrorOccurred(err.Error())
	}
	return call.ReplyImagesPrune(prunedImages)
}