package configmaps

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/containers/common/pkg/configmaps/filedriver"
	"github.com/containers/storage/pkg/lockfile"
	"github.com/containers/storage/pkg/regexp"
	"github.com/containers/storage/pkg/stringid"
	"golang.org/x/exp/maps"
)

// maxConfigMapSize is the max size for configMap data - 512kB
const maxConfigMapSize = 512000

// configMapIDLength is the character length of a configMap ID - 25
const configMapIDLength = 25

// errInvalidPath indicates that the configMaps path is invalid
var errInvalidPath = errors.New("invalid configmaps path")

// ErrNoSuchConfigMap indicates that the configMap does not exist
var ErrNoSuchConfigMap = errors.New("no such configmap")

// errConfigMapNameInUse indicates that the configMap name is already in use
var errConfigMapNameInUse = errors.New("configmap name in use")

// errInvalidConfigMapName indicates that the configMap name is invalid
var errInvalidConfigMapName = errors.New("invalid configmap name")

// errInvalidDriver indicates that the driver type is invalid
var errInvalidDriver = errors.New("invalid driver")

// errInvalidDriverOpt indicates that a driver option is invalid
var errInvalidDriverOpt = errors.New("invalid driver option")

// errAmbiguous indicates that a configMap is ambiguous
var errAmbiguous = errors.New("configmap is ambiguous")

// errDataSize indicates that the configMap data is too large or too small
var errDataSize = errors.New("configmap data must be larger than 0 and less than 512000 bytes")

// configMapsFile is the name of the file that the configMaps database will be stored in
var configMapsFile = "configmaps.json"

// configMapNameRegexp matches valid configMap names
// Allowed: 253 [a-zA-Z0-9-_.] characters, and the start and end character must be [a-zA-Z0-9]
var configMapNameRegexp = regexp.Delayed(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`)

// ConfigMapManager holds information on handling configmaps
type ConfigMapManager struct {
	// configMapDBPath is the path to the db file where configmaps are stored
	configMapDBPath string
	// lockfile is the locker for the configmap file
	lockfile *lockfile.LockFile
	// db is an in-memory cache of the database of configMaps
	db *db
}

// ConfigMap defines a configMap
type ConfigMap struct {
	// Name is the name of the configmap
	Name string `json:"name"`
	// ID is the unique configMap ID
	ID string `json:"id"`
	// Metadata stores other metadata on the configMap
	Metadata map[string]string `json:"metadata,omitempty"`
	// CreatedAt is when the configMap was created
	CreatedAt time.Time `json:"createdAt"`
	// Driver is the driver used to store configMap data
	Driver string `json:"driver"`
	// DriverOptions is other metadata needed to use the driver
	DriverOptions map[string]string `json:"driverOptions"`
}

// ConfigMapsDriver interfaces with the configMaps data store.
// The driver stores the actual bytes of configMap data, as opposed to
// the configMap metadata.
//
// revive does not like the name because the package is already called configmaps
//
//nolint:revive
type ConfigMapsDriver interface {
	// List lists all configMap ids in the configMaps data store
	List() ([]string, error)
	// Lookup gets the configMap's data bytes
	Lookup(id string) ([]byte, error)
	// Store stores the configMap's data bytes
	Store(id string, data []byte) error
	// Delete deletes a configMap's data from the driver
	Delete(id string) error
}

// NewManager creates a new configMaps manager
// rootPath is the directory where the configMaps data file resides
func NewManager(rootPath string) (*ConfigMapManager, error) {
	manager := new(ConfigMapManager)

	if !filepath.IsAbs(rootPath) {
		return nil, fmt.Errorf("path must be absolute: %s: %w", rootPath, errInvalidPath)
	}
	// the lockfile functions require that the rootPath dir is executable
	if err := os.MkdirAll(rootPath, 0o700); err != nil {
		return nil, err
	}

	lock, err := lockfile.GetLockFile(filepath.Join(rootPath, "configMaps.lock"))
	if err != nil {
		return nil, err
	}
	manager.lockfile = lock
	manager.configMapDBPath = filepath.Join(rootPath, configMapsFile)
	manager.db = new(db)
	manager.db.ConfigMaps = make(map[string]ConfigMap)
	manager.db.NameToID = make(map[string]string)
	manager.db.IDToName = make(map[string]string)
	return manager, nil
}

// Store takes a name, creates a configMap and stores the configMap metadata and the configMap payload.
// It returns a generated ID that is associated with the configMap.
// The max size for configMap data is 512kB.
func (s *ConfigMapManager) Store(name string, data []byte, driverType string, driverOpts map[string]string) (string, error) {
	err := validateConfigMapName(name)
	if err != nil {
		return "", err
	}

	if !(len(data) > 0 && len(data) < maxConfigMapSize) {
		return "", errDataSize
	}

	s.lockfile.Lock()
	defer s.lockfile.Unlock()

	exist, err := s.exactConfigMapExists(name)
	if err != nil {
		return "", err
	}
	if exist {
		return "", fmt.Errorf("%s: %w", name, errConfigMapNameInUse)
	}

	secr := new(ConfigMap)
	secr.Name = name

	for {
		newID := stringid.GenerateNonCryptoID()
		// GenerateNonCryptoID() gives 64 characters, so we truncate to correct length
		newID = newID[0:configMapIDLength]
		_, err := s.lookupConfigMap(newID)
		if err != nil {
			if errors.Is(err, ErrNoSuchConfigMap) {
				secr.ID = newID
				break
			}
			return "", err
		}
	}

	secr.Driver = driverType
	secr.Metadata = make(map[string]string)
	secr.CreatedAt = time.Now()
	secr.DriverOptions = driverOpts

	driver, err := getDriver(driverType, driverOpts)
	if err != nil {
		return "", err
	}
	err = driver.Store(secr.ID, data)
	if err != nil {
		return "", fmt.Errorf("creating configMap %s: %w", name, err)
	}

	err = s.store(secr)
	if err != nil {
		return "", fmt.Errorf("creating configMap %s: %w", name, err)
	}

	return secr.ID, nil
}

// Delete removes all configMap metadata and configMap data associated with the specified configMap.
// Delete takes a name, ID, or partial ID.
func (s *ConfigMapManager) Delete(nameOrID string) (string, error) {
	err := validateConfigMapName(nameOrID)
	if err != nil {
		return "", err
	}

	s.lockfile.Lock()
	defer s.lockfile.Unlock()

	configMap, err := s.lookupConfigMap(nameOrID)
	if err != nil {
		return "", err
	}
	configMapID := configMap.ID

	driver, err := getDriver(configMap.Driver, configMap.DriverOptions)
	if err != nil {
		return "", err
	}

	err = driver.Delete(configMapID)
	if err != nil {
		return "", fmt.Errorf("deleting configMap %s: %w", nameOrID, err)
	}

	err = s.delete(configMapID)
	if err != nil {
		return "", fmt.Errorf("deleting configMap %s: %w", nameOrID, err)
	}
	return configMapID, nil
}

// Lookup gives a configMap's metadata given its name, ID, or partial ID.
func (s *ConfigMapManager) Lookup(nameOrID string) (*ConfigMap, error) {
	s.lockfile.Lock()
	defer s.lockfile.Unlock()

	return s.lookupConfigMap(nameOrID)
}

// List lists all configMaps.
func (s *ConfigMapManager) List() ([]ConfigMap, error) {
	s.lockfile.Lock()
	defer s.lockfile.Unlock()

	configMaps, err := s.lookupAll()
	if err != nil {
		return nil, err
	}
	return maps.Values(configMaps), nil
}

// LookupConfigMapData returns configMap metadata as well as configMap data in bytes.
// The configMap data can be looked up using its name, ID, or partial ID.
func (s *ConfigMapManager) LookupConfigMapData(nameOrID string) (*ConfigMap, []byte, error) {
	s.lockfile.Lock()
	defer s.lockfile.Unlock()

	configMap, err := s.lookupConfigMap(nameOrID)
	if err != nil {
		return nil, nil, err
	}
	driver, err := getDriver(configMap.Driver, configMap.DriverOptions)
	if err != nil {
		return nil, nil, err
	}
	data, err := driver.Lookup(configMap.ID)
	if err != nil {
		return nil, nil, err
	}
	return configMap, data, nil
}

// validateConfigMapName checks if the configMap name is valid.
func validateConfigMapName(name string) error {
	if !configMapNameRegexp.MatchString(name) ||
		len(name) > 253 ||
		strings.HasSuffix(name, "-") ||
		strings.HasSuffix(name, ".") ||
		strings.HasSuffix(name, "_") {
		return fmt.Errorf("only 253 [a-zA-Z0-9-_.] characters allowed, and the start and end character must be [a-zA-Z0-9]: %s: %w", name, errInvalidConfigMapName)
	}
	return nil
}

// getDriver creates a new driver.
func getDriver(name string, opts map[string]string) (ConfigMapsDriver, error) {
	if name == "file" {
		if path, ok := opts["path"]; ok {
			return filedriver.NewDriver(path)
		}
		return nil, fmt.Errorf("need path for filedriver: %w", errInvalidDriverOpt)
	}
	return nil, errInvalidDriver
}
