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

package configutil

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/hcl"
	"github.com/hashicorp/hcl/hcl/ast"
)

const (
	UserLockoutThresholdDefault    = 5
	UserLockoutDurationDefault     = 15 * time.Minute
	UserLockoutCounterResetDefault = 15 * time.Minute
	DisableUserLockoutDefault      = false
)

type UserLockout struct {
	Type                   string
	LockoutThreshold       uint64        `hcl:"-"`
	LockoutThresholdRaw    interface{}   `hcl:"lockout_threshold"`
	LockoutDuration        time.Duration `hcl:"-"`
	LockoutDurationRaw     interface{}   `hcl:"lockout_duration"`
	LockoutCounterReset    time.Duration `hcl:"-"`
	LockoutCounterResetRaw interface{}   `hcl:"lockout_counter_reset"`
	DisableLockout         bool          `hcl:"-"`
	DisableLockoutRaw      interface{}   `hcl:"disable_lockout"`
}

func GetSupportedUserLockoutsAuthMethods() []string {
	return []string{"userpass", "approle", "ldap"}
}

func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
	var err error
	result.UserLockouts = make([]*UserLockout, 0, len(list.Items))
	userLockoutsMap := make(map[string]*UserLockout)
	for i, item := range list.Items {
		var userLockoutConfig UserLockout
		if err := hcl.DecodeObject(&userLockoutConfig, item.Val); err != nil {
			return multierror.Prefix(err, fmt.Sprintf("userLockouts.%d:", i))
		}

		// Base values
		{
			switch {
			case userLockoutConfig.Type != "":
			case len(item.Keys) == 1:
				userLockoutConfig.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
			default:
				return multierror.Prefix(errors.New("auth type for user lockout must be specified, if it applies to all auth methods specify \"all\" "), fmt.Sprintf("user_lockouts.%d:", i))
			}

			userLockoutConfig.Type = strings.ToLower(userLockoutConfig.Type)
			// Supported auth methods for user lockout configuration: ldap, approle, userpass
			// "all" is used to apply the configuration to all supported auth methods
			switch userLockoutConfig.Type {
			case "all", "ldap", "approle", "userpass":
				result.found(userLockoutConfig.Type, userLockoutConfig.Type)
			default:
				return multierror.Prefix(fmt.Errorf("unsupported auth type %q", userLockoutConfig.Type), fmt.Sprintf("user_lockouts.%d:", i))
			}
		}

		// Lockout Parameters

		// Not setting raw entries to nil here as soon as they are parsed
		// as they are used to set the missing user lockout configuration values later.
		{
			if userLockoutConfig.LockoutThresholdRaw != nil {
				userLockoutThresholdString := fmt.Sprintf("%v", userLockoutConfig.LockoutThresholdRaw)
				if userLockoutConfig.LockoutThreshold, err = strconv.ParseUint(userLockoutThresholdString, 10, 64); err != nil {
					return multierror.Prefix(fmt.Errorf("error parsing lockout_threshold: %w", err), fmt.Sprintf("user_lockouts.%d", i))
				}
			}

			if userLockoutConfig.LockoutDurationRaw != nil {
				if userLockoutConfig.LockoutDuration, err = parseutil.ParseDurationSecond(userLockoutConfig.LockoutDurationRaw); err != nil {
					return multierror.Prefix(fmt.Errorf("error parsing lockout_duration: %w", err), fmt.Sprintf("user_lockouts.%d", i))
				}
				if userLockoutConfig.LockoutDuration < 0 {
					return multierror.Prefix(errors.New("lockout_duration cannot be negative"), fmt.Sprintf("user_lockouts.%d", i))
				}

			}

			if userLockoutConfig.LockoutCounterResetRaw != nil {
				if userLockoutConfig.LockoutCounterReset, err = parseutil.ParseDurationSecond(userLockoutConfig.LockoutCounterResetRaw); err != nil {
					return multierror.Prefix(fmt.Errorf("error parsing lockout_counter_reset: %w", err), fmt.Sprintf("user_lockouts.%d", i))
				}
				if userLockoutConfig.LockoutCounterReset < 0 {
					return multierror.Prefix(errors.New("lockout_counter_reset cannot be negative"), fmt.Sprintf("user_lockouts.%d", i))
				}

			}
			if userLockoutConfig.DisableLockoutRaw != nil {
				if userLockoutConfig.DisableLockout, err = parseutil.ParseBool(userLockoutConfig.DisableLockoutRaw); err != nil {
					return multierror.Prefix(fmt.Errorf("invalid value for disable_lockout: %w", err), fmt.Sprintf("user_lockouts.%d", i))
				}
			}
		}
		userLockoutsMap[userLockoutConfig.Type] = &userLockoutConfig
	}

	// Use raw entries to set values for user lockout configurations fields
	// that were not configured using config file.
	// The raw entries would mean that the entry was configured by the user using the config file.
	// If any of these fields are not configured using the config file (missing fields),
	// we set values for these fields with defaults
	// The issue with not being able to use non-raw entries is because of fields lockout threshold
	// and disable lockout. We cannot differentiate using non-raw entries if the user configured these fields
	// with values (0 and false) or if the the user did not configure these values in config file at all.
	// The raw fields are set to nil after setting missing values in setNilValuesForRawUserLockoutFields function
	userLockoutsMap = setMissingUserLockoutValuesInMap(userLockoutsMap)
	for _, userLockoutValues := range userLockoutsMap {
		result.UserLockouts = append(result.UserLockouts, userLockoutValues)
	}
	return nil
}

// setUserLockoutValueAllInMap sets default user lockout values for key "all" (all auth methods)
// for user lockout fields that are not configured using config file
func setUserLockoutValueAllInMap(userLockoutAll *UserLockout) *UserLockout {
	if userLockoutAll.Type == "" {
		userLockoutAll.Type = "all"
	}
	if userLockoutAll.LockoutThresholdRaw == nil {
		userLockoutAll.LockoutThreshold = UserLockoutThresholdDefault
	}
	if userLockoutAll.LockoutDurationRaw == nil {
		userLockoutAll.LockoutDuration = UserLockoutDurationDefault
	}
	if userLockoutAll.LockoutCounterResetRaw == nil {
		userLockoutAll.LockoutCounterReset = UserLockoutCounterResetDefault
	}
	if userLockoutAll.DisableLockoutRaw == nil {
		userLockoutAll.DisableLockout = DisableUserLockoutDefault
	}
	return setNilValuesForRawUserLockoutFields(userLockoutAll)
}

// setDefaultUserLockoutValuesInMap sets missing user lockout fields for auth methods
// with default values (from key "all") that are not configured using config file
func setMissingUserLockoutValuesInMap(userLockoutsMap map[string]*UserLockout) map[string]*UserLockout {
	// set values for "all" key with default values for "all" user lockout fields that are not configured
	// the "all" key values will be used as default values for other auth methods
	userLockoutAll, ok := userLockoutsMap["all"]
	switch ok {
	case true:
		userLockoutsMap["all"] = setUserLockoutValueAllInMap(userLockoutAll)
	default:
		userLockoutsMap["all"] = setUserLockoutValueAllInMap(&UserLockout{})
	}

	for _, userLockoutAuth := range userLockoutsMap {
		if userLockoutAuth.Type == "all" {
			continue
		}
		// set missing values
		if userLockoutAuth.LockoutThresholdRaw == nil {
			userLockoutAuth.LockoutThreshold = userLockoutsMap["all"].LockoutThreshold
		}
		if userLockoutAuth.LockoutDurationRaw == nil {
			userLockoutAuth.LockoutDuration = userLockoutsMap["all"].LockoutDuration
		}
		if userLockoutAuth.LockoutCounterResetRaw == nil {
			userLockoutAuth.LockoutCounterReset = userLockoutsMap["all"].LockoutCounterReset
		}
		if userLockoutAuth.DisableLockoutRaw == nil {
			userLockoutAuth.DisableLockout = userLockoutsMap["all"].DisableLockout
		}
		userLockoutAuth = setNilValuesForRawUserLockoutFields(userLockoutAuth)
		userLockoutsMap[userLockoutAuth.Type] = userLockoutAuth
	}
	return userLockoutsMap
}

// setNilValuesForRawUserLockoutFields sets nil values for user lockout Raw fields
func setNilValuesForRawUserLockoutFields(userLockout *UserLockout) *UserLockout {
	userLockout.LockoutThresholdRaw = nil
	userLockout.LockoutDurationRaw = nil
	userLockout.LockoutCounterResetRaw = nil
	userLockout.DisableLockoutRaw = nil
	return userLockout
}
