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

package controller

import (
	"context"

	"github.com/hashicorp/boundary/internal/cmd/config"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/event"
	"github.com/hashicorp/boundary/internal/ratelimit"
	"github.com/hashicorp/go-rate"
)

// limit is a representation of a rate.Limit that is used when emitting a sys
// event to report the rate limit configuration.
type limit struct {
	Resource  string `json:"resource"`
	Action    string `json:"action"`
	Per       string `json:"per"`
	Unlimited bool   `json:"unlimited"`
	Limit     uint64 `json:"limit"`
	Period    string `json:"period"`
}

// Types used for the nested structure of the sys event.
type (
	actionLimits    []limit
	resourceActions map[string]actionLimits
	resources       map[string]resourceActions
)

type rateLimiterConfig struct {
	maxSize  int
	disabled bool
	configs  ratelimit.Configs

	limits []rate.Limit
}

func newRateLimiterConfig(ctx context.Context, configs ratelimit.Configs, maxSize int, disabled bool) (*rateLimiterConfig, error) {
	const op = "controller.newRateLimiterConfig"

	switch {
	case disabled && len(configs) != 0:
		return nil, errors.New(ctx, errors.InvalidConfiguration, op, "disabled rate limiter with rate limit configs")
	}

	var limits []rate.Limit
	var err error
	if !disabled {
		if limits, err = configs.Limits(ctx); err != nil {
			return nil, err
		}
	}

	return &rateLimiterConfig{
		maxSize:  maxSize,
		disabled: disabled,
		configs:  configs,
		limits:   limits,
	}, nil
}

// writeSysEvent writes a sys event for c
func (c *rateLimiterConfig) writeSysEvent(ctx context.Context) {
	const op = "controller.(rateLimiterConfig).writeSysEvent"

	if c.disabled {
		event.WriteSysEvent(
			ctx,
			op,
			"controller api rate limiter",
			"disabled",
			true,
		)
		return
	}

	e := make(resources)

	for _, l := range c.limits {
		var r resourceActions
		var a actionLimits
		var ok bool
		r, ok = e[l.GetResource()]
		if !ok {
			r = make(resourceActions)
			e[l.GetResource()] = r
		}

		a, ok = r[l.GetAction()]
		if !ok {
			a = make(actionLimits, 0, 3)
		}

		switch ll := l.(type) {
		case *rate.Limited:
			a = append(a, limit{
				Resource:  ll.Resource,
				Action:    ll.Action,
				Per:       ll.Per.String(),
				Unlimited: false,
				Limit:     ll.MaxRequests,
				Period:    ll.Period.String(),
			})
		case *rate.Unlimited:
			a = append(a, limit{
				Resource:  ll.Resource,
				Action:    ll.Action,
				Per:       ll.Per.String(),
				Unlimited: true,
			})
		}
		r[l.GetAction()] = a
	}
	event.WriteSysEvent(
		ctx,
		op,
		"controller api rate limiter",
		"limits",
		e,
		"max_size",
		c.maxSize,
	)
}

func (c *Controller) initializeRateLimiter(conf *config.Config) error {
	const op = "controller.(Controller).initializeRateLimiter"
	switch {
	case conf == nil:
		return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config")
	case conf.Controller == nil:
		return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller")
	}

	c.rateLimiterMu.Lock()
	defer c.rateLimiterMu.Unlock()

	rlConfig, err := newRateLimiterConfig(
		c.baseContext,
		conf.Controller.ApiRateLimits,
		conf.Controller.ApiRateLimiterMaxQuotas,
		conf.Controller.ApiRateLimitDisable,
	)
	if err != nil {
		return err
	}

	switch {
	case rlConfig.disabled:
		c.rateLimiter = rate.NopLimiter
	default:
		c.rateLimiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize)
		if err != nil {
			return err
		}
	}

	c.conf.rateLimiterConfig = rlConfig

	rlConfig.writeSysEvent(c.baseContext)
	return nil
}

func (c *Controller) getRateLimiter() ratelimit.Limiter {
	c.rateLimiterMu.RLock()
	defer c.rateLimiterMu.RUnlock()
	return c.rateLimiter
}

// ReloadRateLimiter replaces the Controller's rate.Limiter with a new rate.Limiter
// using the supplied ratelimit.Configs and max entries. If the configs and max
// entries match the current values, the rate.Limiter is not replaced and no
// error is returned. Otherwise the rate.Limiter is replaced, and the old rate.Limiter
// is shutdown. This means that a client's quota is effectively reset if the
// configuration changes.
func (c *Controller) ReloadRateLimiter(newConfig *config.Config) error {
	const op = "controller.(Controller).ReloadRateLimiter"

	switch {
	case newConfig == nil:
		return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config")
	case newConfig.Controller == nil:
		return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller")
	}

	rlConfig, err := newRateLimiterConfig(
		c.baseContext,
		newConfig.Controller.ApiRateLimits,
		newConfig.Controller.ApiRateLimiterMaxQuotas,
		newConfig.Controller.ApiRateLimitDisable,
	)
	if err != nil {
		return err
	}

	// Config has not changed, no need to reload.
	if c.conf.rateLimiterConfig.maxSize == rlConfig.maxSize &&
		c.conf.rateLimiterConfig.disabled == rlConfig.disabled &&
		c.conf.rateLimiterConfig.configs.Equal(rlConfig.configs) {
		return nil
	}

	var limiter ratelimit.Limiter
	switch {
	case rlConfig.disabled:
		limiter = rate.NopLimiter
	default:
		limiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize)
		if err != nil {
			return errors.Wrap(c.baseContext, err, op)
		}

	}
	c.rateLimiterMu.Lock()
	old := c.rateLimiter
	c.rateLimiter = limiter
	c.conf.rateLimiterConfig = rlConfig
	c.rateLimiterMu.Unlock()
	rlConfig.writeSysEvent(c.baseContext)

	return old.Shutdown()
}
