// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package cache

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"

	"github.com/sirupsen/logrus"
)

const registryCacheVersion = "1.0"

type RegistryCache struct {
	Registries map[string]*AuthEntry
	Version    string
}

type fileCredentialCache struct {
	path                 string
	filename             string
	cachePrefixKey       string
	publicCacheKey       string
	legacyCachePrefixKey string
	legacyPublicCacheKey string
}

func newRegistryCache() *RegistryCache {
	return &RegistryCache{
		Registries: make(map[string]*AuthEntry),
		Version:    registryCacheVersion,
	}
}

// NewFileCredentialsCache returns a new file credentials cache.
//
// path is used for temporary files during save, and filename should be a relative filename
// in the same directory where the cache is serialized and deserialized.
//
// cachePrefixKey is used for scoping credentials for a given credential cache (i.e. region and
// accessKey). legacyCachePrefixKey and legacyPublicCacheKey are used for backward compatibility
// with MD5-based cache keys.
func NewFileCredentialsCache(path string, filename string, cachePrefixKey string, publicCacheKey string, legacyCachePrefixKey string, legacyPublicCacheKey string) CredentialsCache {
	if _, err := os.Stat(path); err != nil {
		os.MkdirAll(path, 0700)
	}
	return &fileCredentialCache{
		path:                 path,
		filename:             filename,
		cachePrefixKey:       cachePrefixKey,
		publicCacheKey:       publicCacheKey,
		legacyCachePrefixKey: legacyCachePrefixKey,
		legacyPublicCacheKey: legacyPublicCacheKey,
	}
}

func (f *fileCredentialCache) Get(registry string) *AuthEntry {
	logrus.WithField("registry", registry).Debug("Checking file cache")
	registryCache := f.init()

	entry := registryCache.Registries[f.cachePrefixKey+registry]
	if entry != nil {
		return entry
	}

	if isFipsMode() {
		logrus.WithField("registry", registry).Debug("FIPS mode enabled, skipping legacy MD5 cache lookup")
		return nil
	}

	legacyEntry := registryCache.Registries[f.legacyCachePrefixKey+registry]
	if legacyEntry != nil {
		logrus.WithField("registry", registry).Debug("Found cached credentials using legacy MD5 key")
		return legacyEntry
	}

	logrus.WithField("registry", registry).Debug("Credentials not found")
	return nil
}

func (f *fileCredentialCache) GetPublic() *AuthEntry {
	logrus.Debug("Checking file cache for ECR Public")
	registryCache := f.init()

	entry := registryCache.Registries[f.publicCacheKey]
	if entry != nil {
		return entry
	}

	if isFipsMode() {
		logrus.Debug("FIPS mode enabled, skipping legacy MD5 cache lookup for ECR Public")
		return nil
	}

	legacyEntry := registryCache.Registries[f.legacyPublicCacheKey]
	if legacyEntry != nil {
		logrus.Debug("Found cached ECR Public credentials using legacy MD5 key")
		return legacyEntry
	}

	logrus.WithField("registry", "public").Debug("Credentials not found")
	return nil
}

func (f *fileCredentialCache) Set(registry string, entry *AuthEntry) {
	logrus.
		WithField("registry", registry).
		WithField("service", entry.Service).
		Debug("Saving credentials to file cache")
	registryCache := f.init()

	key := f.cachePrefixKey + registry
	if entry.Service == ServiceECRPublic {
		key = f.publicCacheKey
	}
	registryCache.Registries[key] = entry

	err := f.save(registryCache)
	if err != nil {
		logrus.WithError(err).Info("Could not save cache")
	}
}

// List returns all of the available AuthEntries (regardless of prefix)
func (f *fileCredentialCache) List() []*AuthEntry {
	registryCache := f.init()

	// optimize allocation for copy
	entries := make([]*AuthEntry, 0, len(registryCache.Registries))

	for _, entry := range registryCache.Registries {
		entries = append(entries, entry)
	}

	return entries
}

func (f *fileCredentialCache) Clear() {
	err := os.Remove(f.fullFilePath())
	if err != nil {
		logrus.WithError(err).Info("Could not clear cache")
	}
}

func (f *fileCredentialCache) fullFilePath() string {
	return filepath.Join(f.path, f.filename)
}

// Saves credential cache to disk. This writes to a temporary file first, then moves the file to the config location.
// This eliminates from reading partially written credential files, and reduces (but does not eliminate) concurrent
// file access. There is not guarantee here for handling multiple writes at once since there is no out of process locking.
func (f *fileCredentialCache) save(registryCache *RegistryCache) error {
	file, err := os.CreateTemp(f.path, ".config.json.tmp")
	if err != nil {
		return err
	}

	buff, err := json.MarshalIndent(registryCache, "", "  ")
	if err != nil {
		file.Close()
		os.Remove(file.Name())
		return err
	}

	_, err = file.Write(buff)

	if err != nil {
		file.Close()
		os.Remove(file.Name())
		return err
	}

	file.Close()
	// note this is only atomic when relying on linux syscalls
	os.Rename(file.Name(), f.fullFilePath())
	return err
}

func (f *fileCredentialCache) init() *RegistryCache {
	registryCache, err := f.load()
	if err != nil {
		logrus.WithError(err).Info("Could not load existing cache")
		f.Clear()
		registryCache = newRegistryCache()
	}
	return registryCache
}

// Loading a cache from disk will return errors for malformed or incompatible cache files.
func (f *fileCredentialCache) load() (*RegistryCache, error) {
	registryCache := newRegistryCache()

	file, err := os.Open(f.fullFilePath())
	if os.IsNotExist(err) {
		return registryCache, nil
	}

	if err != nil {
		return nil, err
	}

	defer file.Close()

	if err = json.NewDecoder(file).Decode(&registryCache); err != nil {
		return nil, err
	}

	if registryCache.Version != registryCacheVersion {
		return nil, fmt.Errorf("ecr: Registry cache version %#v is not compatible with %#v, ignoring existing cache",
			registryCache.Version,
			registryCacheVersion)
	}

	// migrate entries
	for key := range registryCache.Registries {
		if registryCache.Registries[key].Service == "" {
			registryCache.Registries[key].Service = ServiceECR
		}
	}

	return registryCache, nil
}
