package authenticator

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sync"

	"github.com/transip/gotransip/v6/jwt"
)

// cacheItem is one named item inside the filesystem cache
type cacheItem struct {
	// Key of the cache item, containing
	Key string `json:"key"`
	// Data containing the content of the cache item
	Data []byte `json:"data"`
}

// FileTokenCache is a cache that takes a path and writes a json marshalled File to it,
// it decodes it when created with the NewFileTokenCache method.
// It has a Set method to save a token by name as jwt.Token
// and a Get method one to get a previously acquired token by name returned as jwt.Token
type FileTokenCache struct {
	// File contains the cache file
	File *os.File
	// CacheItems contains a list of cache items, all of them have a key
	CacheItems []cacheItem `json:"items"`
	// prevent simultaneous cache writes
	writeLock sync.RWMutex
}

// NewFileTokenCache opens or creates a filesystem cache File on the specified path
func NewFileTokenCache(path string) (*FileTokenCache, error) {
	// open the File or create a new one on the given location
	file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		return &FileTokenCache{}, fmt.Errorf("error opening cache File: %w", err)
	}

	cache := FileTokenCache{File: file}

	// try to read the File
	fileContent, err := io.ReadAll(file)
	if err != nil {
		return &FileTokenCache{}, fmt.Errorf("error reading cache File: %w", err)
	}

	if len(fileContent) > 0 {
		// read the cached File data as json
		if err := json.Unmarshal(fileContent, &cache); err != nil {
			return &FileTokenCache{}, fmt.Errorf("error unmarshalling cache File: %w", err)
		}
	}

	return &cache, nil
}

// Set will save a token by name as jwt.Token
func (f *FileTokenCache) Set(key string, token jwt.Token) error {
	for idx, item := range f.CacheItems {
		if item.Key == key {
			f.CacheItems[idx].Data = []byte(token.String())

			// persist this change to the cache File
			return f.writeCacheToFile()
		}
	}

	// if the key did not exist before, we append a new item to the cache item list
	f.CacheItems = append(f.CacheItems, cacheItem{Key: key, Data: []byte(token.String())})

	return f.writeCacheToFile()
}

func (f *FileTokenCache) writeCacheToFile() error {
	// try to convert the cache to json, so we can write it to File
	cacheData, err := json.Marshal(f)
	if err != nil {
		return fmt.Errorf("error marshalling cache File: %w", err)
	}

	f.writeLock.Lock()
	defer f.writeLock.Unlock()
	// write the cache data to the File cache
	if err := f.File.Truncate(0); err != nil {
		return fmt.Errorf("error while truncating cache File: %w", err)
	}
	_, err = f.File.WriteAt(cacheData, 0)

	return err
}

// Get a previously acquired token by name returned as jwt.Token
func (f *FileTokenCache) Get(key string) (jwt.Token, error) {
	for _, item := range f.CacheItems {
		if item.Key == key {
			dataAsString := string(item.Data)

			return jwt.New(dataAsString)
		}
	}

	return jwt.Token{}, nil
}
