package blobcache

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/internal/image"
	"github.com/containers/image/v5/transports"
	"github.com/containers/image/v5/types"
	digest "github.com/opencontainers/go-digest"
)

const (
	compressedNote   = ".compressed"
	decompressedNote = ".decompressed"
)

// BlobCache is an object which saves copies of blobs that are written to it while passing them
// through to some real destination, and which can be queried directly in order to read them
// back.
//
// Implements types.ImageReference.
type BlobCache struct {
	reference types.ImageReference
	// WARNING: The contents of this directory may be accessed concurrently,
	// both within this process and by multiple different processes
	directory string
	compress  types.LayerCompression
}

// NewBlobCache creates a new blob cache that wraps an image reference.  Any blobs which are
// written to the destination image created from the resulting reference will also be stored
// as-is to the specified directory or a temporary directory.
// The compress argument controls whether or not the cache will try to substitute a compressed
// or different version of a blob when preparing the list of layers when reading an image.
func NewBlobCache(ref types.ImageReference, directory string, compress types.LayerCompression) (*BlobCache, error) {
	if directory == "" {
		return nil, fmt.Errorf("error creating cache around reference %q: no directory specified", transports.ImageName(ref))
	}
	switch compress {
	case types.Compress, types.Decompress, types.PreserveOriginal:
		// valid value, accept it
	default:
		return nil, fmt.Errorf("unhandled LayerCompression value %v", compress)
	}
	return &BlobCache{
		reference: ref,
		directory: directory,
		compress:  compress,
	}, nil
}

func (b *BlobCache) Transport() types.ImageTransport {
	return b.reference.Transport()
}

func (b *BlobCache) StringWithinTransport() string {
	return b.reference.StringWithinTransport()
}

func (b *BlobCache) DockerReference() reference.Named {
	return b.reference.DockerReference()
}

func (b *BlobCache) PolicyConfigurationIdentity() string {
	return b.reference.PolicyConfigurationIdentity()
}

func (b *BlobCache) PolicyConfigurationNamespaces() []string {
	return b.reference.PolicyConfigurationNamespaces()
}

func (b *BlobCache) DeleteImage(ctx context.Context, sys *types.SystemContext) error {
	return b.reference.DeleteImage(ctx, sys)
}

// blobPath returns the path appropriate for storing a blob with digest.
func (b *BlobCache) blobPath(digest digest.Digest, isConfig bool) (string, error) {
	if err := digest.Validate(); err != nil { // Make sure digest.String() does not contain any unexpected characters
		return "", err
	}
	baseName := digest.String()
	if isConfig {
		baseName += ".config"
	}
	return filepath.Join(b.directory, baseName), nil
}

// findBlob checks if we have a blob for info in cache (whether a config or not)
// and if so, returns it path and size, and whether it was stored as a config.
// It returns ("", -1, nil) if the blob is not
func (b *BlobCache) findBlob(info types.BlobInfo) (string, int64, bool, error) {
	if info.Digest == "" {
		return "", -1, false, nil
	}

	for _, isConfig := range []bool{false, true} {
		path, err := b.blobPath(info.Digest, isConfig)
		if err != nil {
			return "", -1, false, err
		}
		fileInfo, err := os.Stat(path)
		if err == nil && (info.Size == -1 || info.Size == fileInfo.Size()) {
			return path, fileInfo.Size(), isConfig, nil
		}
		if !os.IsNotExist(err) {
			return "", -1, false, fmt.Errorf("checking size: %w", err)
		}
	}

	return "", -1, false, nil

}

func (b *BlobCache) HasBlob(blobinfo types.BlobInfo) (bool, int64, error) {
	path, size, _, err := b.findBlob(blobinfo)
	if err != nil {
		return false, -1, err
	}
	if path != "" {
		return true, size, nil
	}
	return false, -1, nil
}

func (b *BlobCache) Directory() string {
	return b.directory
}

func (b *BlobCache) ClearCache() error {
	f, err := os.Open(b.directory)
	if err != nil {
		return err
	}
	defer f.Close()
	names, err := f.Readdirnames(-1)
	if err != nil {
		return fmt.Errorf("error reading directory %q: %w", b.directory, err)
	}
	for _, name := range names {
		pathname := filepath.Join(b.directory, name)
		if err = os.RemoveAll(pathname); err != nil {
			return fmt.Errorf("clearing cache for %q: %w", transports.ImageName(b), err)
		}
	}
	return nil
}

func (b *BlobCache) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
	return image.FromReference(ctx, sys, b)
}
