package secrets

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"time"
)

type db struct {
	// Secrets maps a secret id to secret metadata
	Secrets map[string]Secret `json:"secrets"`
	// NameToID maps a secret name to a secret id
	NameToID map[string]string `json:"nameToID"`
	// IDToName maps a secret id to a secret name
	IDToName map[string]string `json:"idToName"`
	// lastModified is the time when the database was last modified on the file system
	lastModified time.Time
}

// loadDB loads database data into the in-memory cache if it has been modified
func (s *SecretsManager) loadDB() error {
	// check if the db file exists
	fileInfo, err := os.Stat(s.secretsDBPath)
	if err != nil {
		if !os.IsExist(err) {
			// If the file doesn't exist, then there's no reason to update the db cache,
			// the db cache will show no entries anyway.
			// The file will be created later on a store()
			return nil
		}
		return err
	}

	// We check if the file has been modified after the last time it was loaded into the cache.
	// If the file has been modified, then we know that our cache is not up-to-date, so we load
	// the db into the cache.
	if s.db.lastModified.Equal(fileInfo.ModTime()) {
		return nil
	}

	file, err := os.Open(s.secretsDBPath)
	if err != nil {
		return err
	}
	defer file.Close()
	if err != nil {
		return err
	}

	byteValue, err := ioutil.ReadAll(file)
	if err != nil {
		return err
	}
	unmarshalled := new(db)
	if err := json.Unmarshal(byteValue, unmarshalled); err != nil {
		return err
	}
	s.db = unmarshalled
	s.db.lastModified = fileInfo.ModTime()

	return nil
}

// getNameAndID takes a secret's name, ID, or partial ID, and returns both its name and full ID.
func (s *SecretsManager) getNameAndID(nameOrID string) (name, id string, err error) {
	name, id, err = s.getExactNameAndID(nameOrID)
	if err == nil {
		return name, id, nil
	} else if !errors.Is(err, ErrNoSuchSecret) {
		return "", "", err
	}

	// ID prefix may have been given, iterate through all IDs.
	// ID and partial ID has a max length of 25, so we return if its greater than that.
	if len(nameOrID) > secretIDLength {
		return "", "", fmt.Errorf("no secret with name or id %q: %w", nameOrID, ErrNoSuchSecret)
	}
	exists := false
	var foundID, foundName string
	for id, name := range s.db.IDToName {
		if strings.HasPrefix(id, nameOrID) {
			if exists {
				return "", "", fmt.Errorf("more than one result secret with prefix %s: %w", nameOrID, errAmbiguous)
			}
			exists = true
			foundID = id
			foundName = name
		}
	}

	if exists {
		return foundName, foundID, nil
	}
	return "", "", fmt.Errorf("no secret with name or id %q: %w", nameOrID, ErrNoSuchSecret)
}

// getExactNameAndID takes a secret's name or ID and returns both its name and full ID.
func (s *SecretsManager) getExactNameAndID(nameOrID string) (name, id string, err error) {
	err = s.loadDB()
	if err != nil {
		return "", "", err
	}
	if name, ok := s.db.IDToName[nameOrID]; ok {
		id := nameOrID
		return name, id, nil
	}

	if id, ok := s.db.NameToID[nameOrID]; ok {
		name := nameOrID
		return name, id, nil
	}

	return "", "", fmt.Errorf("no secret with name or id %q: %w", nameOrID, ErrNoSuchSecret)
}

// exactSecretExists checks if the secret exists, given a name or ID
// Does not match partial name or IDs
func (s *SecretsManager) exactSecretExists(nameOrID string) (bool, error) {
	_, _, err := s.getExactNameAndID(nameOrID)
	if err != nil {
		if errors.Is(err, ErrNoSuchSecret) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

// lookupAll gets all secrets stored.
func (s *SecretsManager) lookupAll() (map[string]Secret, error) {
	err := s.loadDB()
	if err != nil {
		return nil, err
	}
	return s.db.Secrets, nil
}

// lookupSecret returns a secret with the given name, ID, or partial ID.
func (s *SecretsManager) lookupSecret(nameOrID string) (*Secret, error) {
	err := s.loadDB()
	if err != nil {
		return nil, err
	}
	_, id, err := s.getNameAndID(nameOrID)
	if err != nil {
		return nil, err
	}
	allSecrets, err := s.lookupAll()
	if err != nil {
		return nil, err
	}
	if secret, ok := allSecrets[id]; ok {
		return &secret, nil
	}

	return nil, fmt.Errorf("no secret with name or id %q: %w", nameOrID, ErrNoSuchSecret)
}

// Store creates a new secret in the secrets database.
// It deals with only storing metadata, not data payload.
func (s *SecretsManager) store(entry *Secret) error {
	err := s.loadDB()
	if err != nil {
		return err
	}

	s.db.Secrets[entry.ID] = *entry
	s.db.NameToID[entry.Name] = entry.ID
	s.db.IDToName[entry.ID] = entry.Name

	marshalled, err := json.MarshalIndent(s.db, "", "  ")
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(s.secretsDBPath, marshalled, 0o600)
	if err != nil {
		return err
	}

	return nil
}

// delete deletes a secret from the secrets database, given a name, ID, or partial ID.
// It deals with only deleting metadata, not data payload.
func (s *SecretsManager) delete(nameOrID string) error {
	name, id, err := s.getNameAndID(nameOrID)
	if err != nil {
		return err
	}
	err = s.loadDB()
	if err != nil {
		return err
	}
	delete(s.db.Secrets, id)
	delete(s.db.NameToID, name)
	delete(s.db.IDToName, id)
	marshalled, err := json.MarshalIndent(s.db, "", "  ")
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(s.secretsDBPath, marshalled, 0o600)
	if err != nil {
		return err
	}
	return nil
}
