// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package ratelimit

import (
	"context"
	"fmt"
	"reflect"
	"sync"
	"time"

	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/types/action"
	"github.com/hashicorp/boundary/internal/types/resource"
	"github.com/hashicorp/go-rate"

	// Imported to register all actions for all resources
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/groups"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/health"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_catalogs"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_sets"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/hosts"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/managed_groups"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/policies"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/roles"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/session_recordings"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/storage_buckets"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/users"
	_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers"
)

// Defaults used when creating default rate.Limits.
const (
	DefaultInTotalRequestLimit       = 30000
	DefaultIpAddressRequestLimit     = 30000
	DefaultAuthTokenRequestLimit     = 3000
	DefaultPeriod                    = time.Second * 30
	DefaultInTotalListRequestLimit   = 1500
	DefaultIpAddressListRequestLimit = 1500
	DefaultAuthTokenListRequestLimit = 150
	DefaultListPeriod                = time.Second * 30
)

// defaultLimiterMaxQuotas is the default maximum number of quotas that
// can be tracked by the rate limiter.
// This is determined at initialization time based on the number of endpoints.
var defaultLimiterMaxQuotas int

var initDefaultLimiterMaxQuotas sync.Once

// DefaultLimiterMaxQuotas returns the default maximum number of quotas that
// can be tracked by the rate limiter.
func DefaultLimiterMaxQuotas() int {
	initDefaultLimiterMaxQuotas.Do(func() {
		// Calculate the default max number of quotas that the rate limiter can
		// store. This is calculated based on the number of endpoints and a
		// static number of quotas per endpoint. This seems like a reasonable
		// way to determine a sane default value. However, it should be noted
		// that this total is shared across all endpoints, and some endpoints
		// will be used more frequently than others.

		const quotasPerInTotal = 1
		const quotasPerIpAddress = 1000
		const quotasPerAuthToken = 1000

		var endpointCount int
		for _, res := range resource.Map {
			switch res {
			case resource.Unknown, resource.All, resource.Controller:
				continue
			}

			actions, err := action.ActionSetForResource(res)
			if err != nil {
				panic(fmt.Sprintf("No actions registered for resource %q", res.String()))
			}
			endpointCount += len(actions)
		}

		defaultLimiterMaxQuotas = (endpointCount * quotasPerInTotal) +
			(endpointCount * quotasPerAuthToken) +
			(endpointCount * quotasPerIpAddress)
	})
	return defaultLimiterMaxQuotas
}

// Config is used to configure rate limits. Each config is used to specify
// the maximum number of requests that can be made in a time period for the
// corresponding resources and actions.
type Config struct {
	Resources []string      `hcl:"resources"`
	Actions   []string      `hcl:"actions"`
	Per       string        `hcl:"per"`
	Limit     int           `hcl:"limit"`
	PeriodHCL string        `hcl:"period"`
	Period    time.Duration `hcl:"-"`
	Unlimited bool          `hcl:"unlimited"`
}

// Configs is an ordered set of Config.
type Configs []*Config

// Equal checks if a set of Configs is equal to another set of Configs.
func (c Configs) Equal(o Configs) bool {
	return reflect.DeepEqual(c, o)
}

// Limits creates a slice of rate.Limit from the Configs. This will enumerate
// every combination of resource+action, defining a Limit for each.
func (c Configs) Limits(ctx context.Context) ([]rate.Limit, error) {
	const op = "ratelimit.(Configs).Limits"

	defaults := make(map[string]rate.Limit, len(resource.Map)*len(action.Map))

	allResources := make([]resource.Type, 0, len(resource.Map))
	for _, res := range resource.Map {
		switch res {
		case resource.Unknown, resource.All, resource.Controller:
			continue
		}
		allResources = append(allResources, res)
	}

	for _, res := range allResources {
		validActions, err := action.ActionSetForResource(res)
		if err != nil {
			// This shouldn't be possible, since we should have encountered
			// this error during init and panicked already. If for some reason
			// that was not the case, it seems like a good idea to panic here.
			panic(fmt.Sprintf("No actions registered for resource %q", res.String()))
		}

		for a := range validActions {
			inTotalKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerTotal)
			authTokenKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerAuthToken)
			ipAddressKey := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPerIPAddress)
			switch a {
			case action.List:
				defaults[inTotalKey] = &rate.Limited{
					Resource:    res.String(),
					Action:      a.String(),
					Per:         rate.LimitPerTotal,
					MaxRequests: DefaultInTotalListRequestLimit,
					Period:      DefaultListPeriod,
				}
				defaults[authTokenKey] = &rate.Limited{
					Resource:    res.String(),
					Action:      a.String(),
					Per:         rate.LimitPerAuthToken,
					MaxRequests: DefaultAuthTokenListRequestLimit,
					Period:      DefaultListPeriod,
				}
				defaults[ipAddressKey] = &rate.Limited{
					Resource:    res.String(),
					Action:      a.String(),
					Per:         rate.LimitPerIPAddress,
					MaxRequests: DefaultIpAddressListRequestLimit,
					Period:      DefaultListPeriod,
				}
			default:
				defaults[inTotalKey] = &rate.Limited{
					Resource:    res.String(),
					Action:      a.String(),
					Per:         rate.LimitPerTotal,
					MaxRequests: DefaultInTotalRequestLimit,
					Period:      DefaultPeriod,
				}
				defaults[authTokenKey] = &rate.Limited{
					Resource:    res.String(),
					Action:      a.String(),
					Per:         rate.LimitPerAuthToken,
					MaxRequests: DefaultAuthTokenRequestLimit,
					Period:      DefaultPeriod,
				}
				defaults[ipAddressKey] = &rate.Limited{
					Resource:    res.String(),
					Action:      a.String(),
					Per:         rate.LimitPerIPAddress,
					MaxRequests: DefaultIpAddressRequestLimit,
					Period:      DefaultPeriod,
				}
			}
		}
	}

	for _, cc := range c {
		var resourceSet []resource.Type
		wildcardResources := len(cc.Resources) == 1 && cc.Resources[0] == resource.All.String()
		switch {
		case wildcardResources:
			resourceSet = allResources
		default:
			for _, r := range cc.Resources {
				rr, ok := resource.Map[r]
				if !ok {
					return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("unknown resource %s", r))
				}
				resourceSet = append(resourceSet, rr)
			}
		}

		switch {
		case len(cc.Actions) == 1 && cc.Actions[0] == action.All.String():
			for _, res := range resourceSet {
				validActions, err := action.ActionSetForResource(res)
				if err != nil {
					return nil, err
				}
				for a := range validActions {
					key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per))

					switch {
					case cc.Unlimited:
						defaults[key] = &rate.Unlimited{
							Resource: res.String(),
							Action:   a.String(),
							Per:      rate.LimitPer(cc.Per),
						}
					default:
						defaults[key] = &rate.Limited{
							Resource:    res.String(),
							Action:      a.String(),
							Per:         rate.LimitPer(cc.Per),
							MaxRequests: uint64(cc.Limit),
							Period:      cc.Period,
						}
					}
				}
			}
		default:
			for _, res := range resourceSet {
				validActions, err := action.ActionSetForResource(res)
				if err != nil {
					return nil, err
				}
				validActionMap := make(map[string]action.Type, len(validActions))
				for a := range validActions {
					validActionMap[a.String()] = a
				}

				for _, aStr := range cc.Actions {
					a, ok := validActionMap[aStr]
					if !ok {
						if wildcardResources {
							// For wildcard resources, verify the action exists in the action system
							// before skipping. If it doesn't exist at all, return an error.
							if _, exists := action.Map[aStr]; !exists {
								return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("action %s not valid for resource %s", aStr, resource.All.String()))
							}
							// When using wildcard resources (*), skip actions that aren't valid for
							// the current resource type rather than returning an error. This allows
							// specifying rate limits like "read,list" for all resources, even though
							// not all resources support both actions (e.g., sessions don't support "read").
							continue
						}
						return nil, errors.New(ctx, errors.InvalidConfiguration, op, "", errors.WithMsg("action %s not valid for resource %s", aStr, res.String()))
					}
					key := fmt.Sprintf("%s:%s:%s", res.String(), a.String(), rate.LimitPer(cc.Per))

					switch {
					case cc.Unlimited:
						defaults[key] = &rate.Unlimited{
							Resource: res.String(),
							Action:   a.String(),
							Per:      rate.LimitPer(cc.Per),
						}
					default:
						defaults[key] = &rate.Limited{
							Resource:    res.String(),
							Action:      a.String(),
							Per:         rate.LimitPer(cc.Per),
							MaxRequests: uint64(cc.Limit),
							Period:      cc.Period,
						}
					}
				}
			}
		}
	}

	limits := make([]rate.Limit, 0, len(defaults))
	for _, v := range defaults {
		limits = append(limits, v)
	}
	return limits, nil
}
