// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package swift

import (
	"context"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	log "github.com/hashicorp/go-hclog"

	metrics "github.com/armon/go-metrics"
	cleanhttp "github.com/hashicorp/go-cleanhttp"
	"github.com/hashicorp/go-secure-stdlib/strutil"
	"github.com/hashicorp/vault/sdk/physical"
	"github.com/ncw/swift"
)

// Verify SwiftBackend satisfies the correct interfaces
var _ physical.Backend = (*SwiftBackend)(nil)

// SwiftBackend is a physical backend that stores data
// within an OpenStack Swift container.
type SwiftBackend struct {
	container  string
	client     *swift.Connection
	logger     log.Logger
	permitPool *physical.PermitPool
}

// NewSwiftBackend constructs a Swift backend using a pre-existing
// container. Credentials can be provided to the backend, sourced
// from the environment.
func NewSwiftBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
	var ok bool

	username := os.Getenv("OS_USERNAME")
	if username == "" {
		username = conf["username"]
		if username == "" {
			return nil, fmt.Errorf("missing username")
		}
	}
	password := os.Getenv("OS_PASSWORD")
	if password == "" {
		password = conf["password"]
		if password == "" {
			return nil, fmt.Errorf("missing password")
		}
	}
	authUrl := os.Getenv("OS_AUTH_URL")
	if authUrl == "" {
		authUrl = conf["auth_url"]
		if authUrl == "" {
			return nil, fmt.Errorf("missing auth_url")
		}
	}
	container := os.Getenv("OS_CONTAINER")
	if container == "" {
		container = conf["container"]
		if container == "" {
			return nil, fmt.Errorf("missing container")
		}
	}
	project := os.Getenv("OS_PROJECT_NAME")
	if project == "" {
		if project, ok = conf["project"]; !ok {
			// Check for KeyStone naming prior to V3
			project = os.Getenv("OS_TENANT_NAME")
			if project == "" {
				project = conf["tenant"]
			}
		}
	}

	domain := os.Getenv("OS_USER_DOMAIN_NAME")
	if domain == "" {
		domain = conf["domain"]
	}
	projectDomain := os.Getenv("OS_PROJECT_DOMAIN_NAME")
	if projectDomain == "" {
		projectDomain = conf["project-domain"]
	}

	region := os.Getenv("OS_REGION_NAME")
	if region == "" {
		region = conf["region"]
	}
	tenantID := os.Getenv("OS_TENANT_ID")
	if tenantID == "" {
		tenantID = conf["tenant_id"]
	}
	trustID := os.Getenv("OS_TRUST_ID")
	if trustID == "" {
		trustID = conf["trust_id"]
	}
	storageUrl := os.Getenv("OS_STORAGE_URL")
	if storageUrl == "" {
		storageUrl = conf["storage_url"]
	}
	authToken := os.Getenv("OS_AUTH_TOKEN")
	if authToken == "" {
		authToken = conf["auth_token"]
	}

	c := swift.Connection{
		Domain:       domain,
		UserName:     username,
		ApiKey:       password,
		AuthUrl:      authUrl,
		Tenant:       project,
		TenantDomain: projectDomain,
		Region:       region,
		TenantId:     tenantID,
		TrustId:      trustID,
		StorageUrl:   storageUrl,
		AuthToken:    authToken,
		Transport:    cleanhttp.DefaultPooledTransport(),
	}

	err := c.Authenticate()
	if err != nil {
		return nil, err
	}

	_, _, err = c.Container(container)
	if err != nil {
		return nil, fmt.Errorf("Unable to access container %q: %w", container, err)
	}

	maxParStr, ok := conf["max_parallel"]
	var maxParInt int
	if ok {
		maxParInt, err = strconv.Atoi(maxParStr)
		if err != nil {
			return nil, fmt.Errorf("failed parsing max_parallel parameter: %w", err)
		}
		if logger.IsDebug() {
			logger.Debug("max_parallel set", "max_parallel", maxParInt)
		}
	}

	s := &SwiftBackend{
		client:     &c,
		container:  container,
		logger:     logger,
		permitPool: physical.NewPermitPool(maxParInt),
	}
	return s, nil
}

// Put is used to insert or update an entry
func (s *SwiftBackend) Put(ctx context.Context, entry *physical.Entry) error {
	defer metrics.MeasureSince([]string{"swift", "put"}, time.Now())

	s.permitPool.Acquire()
	defer s.permitPool.Release()

	err := s.client.ObjectPutBytes(s.container, entry.Key, entry.Value, "")
	if err != nil {
		return err
	}

	return nil
}

// Get is used to fetch an entry
func (s *SwiftBackend) Get(ctx context.Context, key string) (*physical.Entry, error) {
	defer metrics.MeasureSince([]string{"swift", "get"}, time.Now())

	s.permitPool.Acquire()
	defer s.permitPool.Release()

	// Do a list of names with the key first since eventual consistency means
	// it might be deleted, but a node might return a read of bytes which fails
	// the physical test
	list, err := s.client.ObjectNames(s.container, &swift.ObjectsOpts{Prefix: key})
	if err != nil {
		return nil, err
	}
	if 0 == len(list) {
		return nil, nil
	}
	data, err := s.client.ObjectGetBytes(s.container, key)
	if err == swift.ObjectNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	ent := &physical.Entry{
		Key:   key,
		Value: data,
	}

	return ent, nil
}

// Delete is used to permanently delete an entry
func (s *SwiftBackend) Delete(ctx context.Context, key string) error {
	defer metrics.MeasureSince([]string{"swift", "delete"}, time.Now())

	s.permitPool.Acquire()
	defer s.permitPool.Release()

	err := s.client.ObjectDelete(s.container, key)

	if err != nil && err != swift.ObjectNotFound {
		return err
	}

	return nil
}

// List is used to list all the keys under a given
// prefix, up to the next prefix.
func (s *SwiftBackend) List(ctx context.Context, prefix string) ([]string, error) {
	defer metrics.MeasureSince([]string{"swift", "list"}, time.Now())

	s.permitPool.Acquire()
	defer s.permitPool.Release()

	list, err := s.client.ObjectNamesAll(s.container, &swift.ObjectsOpts{Prefix: prefix})
	if nil != err {
		return nil, err
	}

	keys := []string{}
	for _, key := range list {
		key := strings.TrimPrefix(key, prefix)

		if i := strings.Index(key, "/"); i == -1 {
			// Add objects only from the current 'folder'
			keys = append(keys, key)
		} else if i != -1 {
			// Add truncated 'folder' paths
			keys = strutil.AppendIfMissing(keys, key[:i+1])
		}
	}

	sort.Strings(keys)

	return keys, nil
}
