package cache

import (
	"encoding/json"
	"fmt"
	"io"
	"runtime"

	"github.com/buildpacks/imgutil"
	"github.com/buildpacks/imgutil/remote"
	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/pkg/errors"

	"github.com/buildpacks/lifecycle/image"
	"github.com/buildpacks/lifecycle/log"
	"github.com/buildpacks/lifecycle/platform"
)

const MetadataLabel = "io.buildpacks.lifecycle.cache.metadata"

type ImageCache struct {
	committed    bool
	origImage    imgutil.Image
	newImage     imgutil.Image
	logger       log.Logger
	imageDeleter ImageDeleter
}

// NewImageCache creates a new ImageCache instance
func NewImageCache(origImage imgutil.Image, newImage imgutil.Image, logger log.Logger, imageDeleter ImageDeleter) *ImageCache {
	return &ImageCache{
		origImage:    origImage,
		newImage:     newImage,
		logger:       logger,
		imageDeleter: imageDeleter,
	}
}

// NewImageCacheFromName creates a new ImageCache from the name that has been provided
func NewImageCacheFromName(name string, keychain authn.Keychain, logger log.Logger, imageDeleter ImageDeleter, insecureRegistries ...string) (*ImageCache, error) {
	opts := image.GetInsecureOptions(insecureRegistries)

	origImage, err := remote.NewImage(
		name,
		keychain,
		append(opts,
			remote.FromBaseImage(name),
			remote.WithDefaultPlatform(imgutil.Platform{OS: runtime.GOOS}),
		)...,
	)
	if err != nil {
		return nil, fmt.Errorf("accessing cache image %q: %v", name, err)
	}
	emptyImage, err := remote.NewImage(
		name,
		keychain,
		append(opts,
			remote.WithPreviousImage(name),
			remote.WithDefaultPlatform(imgutil.Platform{OS: runtime.GOOS}),
			remote.AddEmptyLayerOnSave(),
		)...,
	)
	if err != nil {
		return nil, fmt.Errorf("creating new cache image %q: %v", name, err)
	}

	return NewImageCache(origImage, emptyImage, logger, imageDeleter), nil
}

func (c *ImageCache) Exists() bool {
	return c.origImage.Found()
}

func (c *ImageCache) Name() string {
	return c.origImage.Name()
}

func (c *ImageCache) SetMetadata(metadata platform.CacheMetadata) error {
	if c.committed {
		return errCacheCommitted
	}
	data, err := json.Marshal(metadata)
	if err != nil {
		return errors.Wrap(err, "serializing metadata")
	}
	return c.newImage.SetLabel(MetadataLabel, string(data))
}

func (c *ImageCache) RetrieveMetadata() (platform.CacheMetadata, error) {
	if c.origImage.Found() && !c.origImage.Valid() {
		c.logger.Infof("Ignoring cache image %q because it was corrupt", c.origImage.Name())
		return platform.CacheMetadata{}, nil
	}
	var meta platform.CacheMetadata
	if err := image.DecodeLabel(c.origImage, MetadataLabel, &meta); err != nil {
		return platform.CacheMetadata{}, nil
	}
	return meta, nil
}

func (c *ImageCache) AddLayerFile(tarPath string, diffID string) error {
	if c.committed {
		return errCacheCommitted
	}
	return c.newImage.AddLayerWithDiffID(tarPath, diffID)
}

// isLayerNotFound checks if the error is a layer not found error
//
// FIXME: we should not have to rely on trapping ErrUnexpectedEOF.
// If a blob is not present in the registry, we should get imgutil.ErrLayerNotFound,
// but we do not and instead get io.ErrUnexpectedEOF
func isLayerNotFound(err error) bool {
	var e imgutil.ErrLayerNotFound
	return errors.As(err, &e) || errors.Is(err, io.ErrUnexpectedEOF)
}

func (c *ImageCache) ReuseLayer(diffID string) error {
	if c.committed {
		return errCacheCommitted
	}
	err := c.newImage.ReuseLayer(diffID)
	if err != nil {
		// FIXME: this path is not currently executed.
		// If a blob is not present in the registry, we should get imgutil.ErrLayerNotFound.
		// We should then skip attempting to reuse the layer.
		// However, we do not get imgutil.ErrLayerNotFound when the blob is not present.
		if isLayerNotFound(err) {
			return NewReadErr(fmt.Sprintf("failed to find cache layer with SHA '%s'", diffID))
		}
		return fmt.Errorf("failed to reuse cache layer with SHA '%s'", diffID)
	}
	return nil
}

// RetrieveLayer retrieves a layer from the cache
func (c *ImageCache) RetrieveLayer(diffID string) (io.ReadCloser, error) {
	closer, err := c.origImage.GetLayer(diffID)
	if err != nil {
		if isLayerNotFound(err) {
			return nil, NewReadErr(fmt.Sprintf("failed to find cache layer with SHA '%s'", diffID))
		}
		return nil, fmt.Errorf("failed to get cache layer with SHA '%s'", diffID)
	}
	return closer, nil
}

func (c *ImageCache) Commit() error {
	if c.committed {
		return errCacheCommitted
	}

	if err := c.newImage.Save(); err != nil {
		return errors.Wrapf(err, "saving image '%s'", c.newImage.Name())
	}
	c.committed = true

	// Check if the cache image exists prior to saving the new cache at that same location
	if c.origImage.Found() {
		c.imageDeleter.DeleteOrigImageIfDifferentFromNewImage(c.origImage, c.newImage)
	}

	c.origImage = c.newImage

	return nil
}

// VerifyLayer returns an error if the layer contents do not match the provided sha.
func (c *ImageCache) VerifyLayer(_ string) error {
	// we assume the registry is verifying digests for us
	return nil
}
