package testauth

import (
	"fmt"
	"sync"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/consul/authmethod"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/go-uuid"
)

func init() {
	authmethod.Register("testing", newValidator)
}

var (
	tokenDatabaseMu sync.Mutex
	tokenDatabase   map[string]map[string]map[string]string // session => token => fieldmap
)

func StartSession() string {
	sessionID, err := uuid.GenerateUUID()
	if err != nil {
		panic(err)
	}
	return sessionID
}

func ResetSession(sessionID string) {
	tokenDatabaseMu.Lock()
	defer tokenDatabaseMu.Unlock()
	if tokenDatabase != nil {
		delete(tokenDatabase, sessionID)
	}
}

func InstallSessionToken(sessionID string, token string, namespace, name, uid string) {
	fields := map[string]string{
		serviceAccountNamespaceField: namespace,
		serviceAccountNameField:      name,
		serviceAccountUIDField:       uid,
	}

	tokenDatabaseMu.Lock()
	defer tokenDatabaseMu.Unlock()
	if tokenDatabase == nil {
		tokenDatabase = make(map[string]map[string]map[string]string)
	}
	sdb, ok := tokenDatabase[sessionID]
	if !ok {
		sdb = make(map[string]map[string]string)
		tokenDatabase[sessionID] = sdb
	}
	sdb[token] = fields
}

func GetSessionToken(sessionID string, token string) (map[string]string, bool) {
	tokenDatabaseMu.Lock()
	defer tokenDatabaseMu.Unlock()
	if tokenDatabase == nil {
		return nil, false
	}
	sdb, ok := tokenDatabase[sessionID]
	if !ok {
		return nil, false
	}
	fields, ok := sdb[token]
	if !ok {
		return nil, false
	}

	fmCopy := make(map[string]string)
	for k, v := range fields {
		fmCopy[k] = v
	}

	return fmCopy, true
}

type Config struct {
	SessionID string // unique identifier for this set of tokens in the database

	enterpriseConfig `mapstructure:",squash"`
}

func newValidator(method *structs.ACLAuthMethod) (authmethod.Validator, error) {
	if method.Type != "testing" {
		return nil, fmt.Errorf("%q is not a testing auth method", method.Name)
	}

	var config Config
	if err := authmethod.ParseConfig(method.Config, &config); err != nil {
		return nil, err
	}

	if config.SessionID == "" {
		// If you don't explicitly create one, we create a random one but you
		// won't have access to it. Useful if you are testing everything EXCEPT
		// ValidateToken().
		config.SessionID = StartSession()
	}

	return &Validator{
		name:   method.Name,
		config: &config,
	}, nil
}

type Validator struct {
	name   string
	config *Config
}

func (v *Validator) Name() string { return v.name }

// ValidateLogin takes raw user-provided auth method metadata and ensures it is
// sane, provably correct, and currently valid. Relevant identifying data is
// extracted and returned for immediate use by the role binding process.
//
// Depending upon the method, it may make sense to use these calls to continue
// to extend the life of the underlying token.
//
// Returns auth method specific metadata suitable for the Role Binding process.
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, *structs.EnterpriseMeta, error) {
	fields, valid := GetSessionToken(v.config.SessionID, loginToken)
	if !valid {
		return nil, nil, acl.ErrNotFound
	}

	return fields, v.testAuthEntMetaFromFields(fields), nil
}

func (v *Validator) AvailableFields() []string { return availableFields }

const (
	serviceAccountNamespaceField = "serviceaccount.namespace"
	serviceAccountNameField      = "serviceaccount.name"
	serviceAccountUIDField       = "serviceaccount.uid"
)

var availableFields = []string{
	serviceAccountNamespaceField,
	serviceAccountNameField,
	serviceAccountUIDField,
}

// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
// into a structure suitable for selection with a binding rule.
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
	return &selectableVars{
		ServiceAccount: selectableServiceAccount{
			Namespace: fieldMap[serviceAccountNamespaceField],
			Name:      fieldMap[serviceAccountNameField],
			UID:       fieldMap[serviceAccountUIDField],
		},
	}
}

type selectableVars struct {
	ServiceAccount selectableServiceAccount `bexpr:"serviceaccount"`
}

type selectableServiceAccount struct {
	Namespace string `bexpr:"namespace"`
	Name      string `bexpr:"name"`
	UID       string `bexpr:"uid"`
}
