package image

import (
	"fmt"
	"io"
	"os"

	"github.com/containers/image/docker/reference"
	"github.com/containers/storage"
	"github.com/pkg/errors"
	"github.com/projectatomic/libpod/libpod"
	"github.com/projectatomic/libpod/pkg/inspect"
)

// Image is the primary struct for dealing with images
// It is still very much a work in progress
type Image struct {
	inspect.ImageData
	InputName string
	Local     bool
	runtime   *libpod.Runtime
	image     *storage.Image
}

// NewFromLocal creates a new image object that is intended
// to only deal with local images already in the store (or
// its aliases)
func NewFromLocal(name string, runtime *libpod.Runtime) (Image, error) {
	image := Image{
		InputName: name,
		Local:     true,
		runtime:   runtime,
	}
	localImage, err := image.getLocalImage()
	if err != nil {
		return Image{}, err
	}
	image.image = localImage
	return image, nil
}

// New creates a new image object where the image could be local
// or remote
func New(name string, runtime *libpod.Runtime) (Image, error) {
	// We don't know if the image is local or not ... check local first
	newImage := Image{
		InputName: name,
		Local:     false,
		runtime:   runtime,
	}
	localImage, err := newImage.getLocalImage()
	if err == nil {
		newImage.Local = true
		newImage.image = localImage
		return newImage, nil
	}

	// The image is not local
	pullNames, err := newImage.createNamesToPull()
	if err != nil {
		return newImage, err
	}
	if len(pullNames) == 0 {
		return newImage, errors.Errorf("unable to pull %s", newImage.InputName)
	}
	var writer io.Writer
	writer = os.Stderr
	for _, p := range pullNames {
		_, err := newImage.pull(p, writer, runtime)
		if err == nil {
			newImage.InputName = p
			img, err := newImage.getLocalImage()
			newImage.image = img
			return newImage, err
		}
	}
	return newImage, errors.Errorf("unable to find %s", name)
}

// getLocalImage resolves an unknown input describing an image and
// returns a storage.Image or an error. It is used by NewFromLocal.
func (i *Image) getLocalImage() (*storage.Image, error) {
	imageError := fmt.Sprintf("unable to find '%s' in local storage\n", i.InputName)
	if i.InputName == "" {
		return nil, errors.Errorf("input name is blank")
	}
	var taggedName string
	img, err := i.runtime.GetImage(i.InputName)
	if err == nil {
		return img, err
	}

	// container-storage wasn't able to find it in its current form
	// check if the input name has a tag, and if not, run it through
	// again
	decomposedImage, err := decompose(i.InputName)
	if err != nil {
		return nil, err
	}
	// the inputname isn't tagged, so we assume latest and try again
	if !decomposedImage.isTagged {
		taggedName = fmt.Sprintf("%s:latest", i.InputName)
		img, err = i.runtime.GetImage(taggedName)
		if err == nil {
			return img, nil
		}
	}
	hasReg, err := i.hasRegistry()
	if err != nil {
		return nil, errors.Wrapf(err, imageError)
	}

	// if the input name has a registry in it, the image isnt here
	if hasReg {
		return nil, errors.Errorf("%s", imageError)
	}

	// grab all the local images
	images, err := i.runtime.GetImages(&libpod.ImageFilterParams{})
	if err != nil {
		return nil, err
	}

	// check the repotags of all images for a match
	repoImage, err := findImageInRepotags(decomposedImage, images)
	if err == nil {
		return repoImage, nil
	}

	return nil, errors.Errorf("%s", imageError)
}

// hasRegistry returns a bool/err response if the image has a registry in its
// name
func (i *Image) hasRegistry() (bool, error) {
	imgRef, err := reference.Parse(i.InputName)
	if err != nil {
		return false, err
	}
	registry := reference.Domain(imgRef.(reference.Named))
	if registry != "" {
		return true, nil
	}
	return false, nil
}

// ID returns the image ID as a string
func (i *Image) ID() string {
	return i.image.ID
}

// createNamesToPull looks at a decomposed image and determines the possible
// images names to try pulling in combination with the registries.conf file as well
func (i *Image) createNamesToPull() ([]string, error) {
	var pullNames []string
	decomposedImage, err := decompose(i.InputName)
	if err != nil {
		return nil, err
	}

	if decomposedImage.hasRegistry {
		pullNames = append(pullNames, i.InputName)
	} else {
		registries, err := libpod.GetRegistries()
		if err != nil {
			return nil, err
		}
		for _, registry := range registries {
			decomposedImage.registry = registry
			pullNames = append(pullNames, decomposedImage.assemble())
		}
	}
	return pullNames, nil
}

// pull is a temporary function for stage1 to be able to pull images during the image
// resolution tests.  it will be replaced in stage2 with a more robust function.
func (i *Image) pull(name string, writer io.Writer, r *libpod.Runtime) (string, error) {
	options := libpod.CopyOptions{
		Writer:              writer,
		SignaturePolicyPath: r.GetConfig().SignaturePolicyPath,
	}
	return i.runtime.PullImage(name, options)
}

// Remove an image
// This function is only complete enough for the stage 1 tests.
func (i *Image) Remove(force bool) error {
	_, err := i.runtime.RemoveImage(i.image, force)
	return err
}