package images

import (
	"cmp"
	"strings"
	"time"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/dataservices"
	"github.com/portainer/portainer/api/internal/registryutils"

	"github.com/patrickmn/go-cache"
	"github.com/pkg/errors"
)

var registriesCache = cache.New(5*time.Minute, 5*time.Minute)

type RegistryClient struct {
	dataStore dataservices.DataStore
}

func NewRegistryClient(dataStore dataservices.DataStore) *RegistryClient {
	return &RegistryClient{dataStore: dataStore}
}

func (c *RegistryClient) RegistryAuth(image Image) (string, string, error) {
	registry, err := cachedRegistry(image.Opts.Name)
	if err != nil {
		var registries []portainer.Registry
		err = c.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
			var err error
			registries, err = tx.Registry().ReadAll()
			return err
		})
		if err != nil {
			return "", "", err
		}

		registry, err = findBestMatchRegistry(image.Opts.Name, registries)
		if err != nil {
			return "", "", err
		}
	}

	if !registry.Authentication {
		return "", "", errors.New("authentication is disabled")
	}

	return c.CertainRegistryAuth(registry)
}

func (c *RegistryClient) CertainRegistryAuth(registry *portainer.Registry) (string, string, error) {
	if err := registryutils.EnsureRegTokenValid(c.dataStore, registry); err != nil {
		return "", "", err
	}

	if !registry.Authentication {
		return "", "", errors.New("authentication is disabled")
	}

	return registryutils.GetRegEffectiveCredential(registry)
}

func (c *RegistryClient) EncodedRegistryAuth(image Image) (string, error) {
	registry, err := cachedRegistry(image.Opts.Name)
	if err != nil {
		var registries []portainer.Registry
		err = c.dataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
			var err error
			registries, err = tx.Registry().ReadAll()
			return err
		})
		if err != nil {
			return "", err
		}

		registry, err = findBestMatchRegistry(image.Opts.Name, registries)
		if err != nil {
			return "", err
		}
	}

	if !registry.Authentication {
		return "", errors.New("authentication is disabled")
	}

	return c.EncodedCertainRegistryAuth(registry)
}

func (c *RegistryClient) EncodedCertainRegistryAuth(registry *portainer.Registry) (string, error) {
	if err := registryutils.EnsureRegTokenValid(c.dataStore, registry); err != nil {
		return "", err
	}

	return registryutils.GetRegistryAuthHeader(registry)
}

// findBestMatchRegistry finds out the best match registry for repository @Meng
// matching precedence:
// 1. both domain name and username matched (for dockerhub only)
// 2. only URL matched
// 3. pick up the first dockerhub registry
func findBestMatchRegistry(repository string, registries []portainer.Registry) (*portainer.Registry, error) {
	cachedRegistry, err := cachedRegistry(repository)
	if err == nil {
		return cachedRegistry, nil
	}

	var match1, match2, match3 *portainer.Registry

	for _, registry := range registries {
		if strings.Contains(repository, registry.URL) {
			match2 = &registry
		}

		if registry.Type != portainer.DockerHubRegistry {
			continue
		}

		// try to match repository examples:
		//   <USERNAME>/nginx:latest
		//   docker.io/<USERNAME>/nginx:latest
		if strings.HasPrefix(repository, registry.Username+"/") || strings.HasPrefix(repository, registry.URL+"/"+registry.Username+"/") {
			match1 = &registry
		}

		// try to match repository examples:
		//   portainer/portainer-ee:latest
		//   <NON-USERNAME>/portainer-ee:latest
		if match3 == nil {
			match3 = &registry
		}
	}

	match := cmp.Or(match1, match2, match3)
	if match == nil {
		return nil, errors.New("no registries matched")
	}

	registriesCache.Set(repository, *match, 0)

	return match, nil
}

func cachedRegistry(cacheKey string) (*portainer.Registry, error) {
	r, ok := registriesCache.Get(cacheKey)
	if ok {
		registry, ok := r.(portainer.Registry)
		if ok {
			return &registry, nil
		}
	}

	return nil, errors.Errorf("no registry found in cache: %s", cacheKey)
}
