package oci

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

	"github.com/containers/image/v5/types"
)

type CacheOpt func(*Cache) error

func OptSetCachePath(path string) CacheOpt {
	return func(s *Cache) error {
		s.path = path
		return nil
	}
}

type Cache struct {
	path string
}

func (c *Cache) rootfsDir() string {
	return filepath.Join(c.path, rootfsPrefix)
}

func (c *Cache) blobDir() string {
	return filepath.Join(c.path, blobPrefix)
}

// checkEntry maps the given id a path within the cache. It will return an os.ErrNotExist if the entry path is empty.
func (c *Cache) checkEntry(id string) (string, error) {
	path := filepath.Join(c.rootfsDir(), id)
	fi, err := os.Stat(path)
	if err != nil {
		return "", err
	}

	if !fi.IsDir() {
		return "", fmt.Errorf("invalid entry %q is not a directory", path)
	}

	return path, nil
}

func (c *Cache) createEntry(id string) (string, error) {
	path := filepath.Join(c.rootfsDir(), id)
	if err := os.MkdirAll(path, 0755); err != nil {
		return "", err
	}

	return path, nil
}

func NewCache(opts ...CacheOpt) (*Cache, error) {
	s := &Cache{
		path: filepath.Join(defaultCachePath),
	}

	for _, o := range opts {
		if err := o(s); err != nil {
			return nil, err
		}
	}

	if err := os.MkdirAll(s.path, 0755); err != nil {
		return nil, err
	}

	return s, nil
}

func (c *Cache) Pull(ctx context.Context, uri string, sysCtx *types.SystemContext) (string, error) {
	p, err := NewPuller(
		OptSetBlobCachePath(c.blobDir()),
		OptSetSystemContext(sysCtx),
	)
	if err != nil {
		return "", err
	}

	id, err := p.GenerateID(ctx, uri)
	if err != nil {
		return "", err
	}

	// look up cache entry and return if it exists
	path, err := c.checkEntry(id)
	if err == nil {
		return path, nil
	} else if !os.IsNotExist(err) {
		return "", err
	}

	// cache entry does not exist so we must create it
	path, err = c.createEntry(id)
	if err != nil {
		return "", err
	}

	// populate entry
	if err := p.Pull(ctx, uri, path); err != nil {
		// clean up entry on error
		os.RemoveAll(path)
		return "", err
	}

	return path, nil
}
