package engine

import (
	"bytes"
	"context"
	"errors"
	"log/slog"
	"strconv"
)

type RateLimiterCtxKey string

const (
	projectIDCtxValueKey RateLimiterCtxKey = "project_id"
	eventTypeCtxValueKey RateLimiterCtxKey = "event_type"
)

var (
	errRateLimited = errors.New("event is ignored because of the rate limit")
)

type AllowLimiter interface {
	Allow(ctx context.Context) bool
}

// workflowRateLimiter wraps a generic allowLimiter to provide a more suitable interface.
// This type mostly exists because the redistool.TokenLimiter implements this interface, too.
// NOTE: we may consider refactoring the redistool.TokenLimiter and metric.AllowLimiter
// to be more generic and not needing the HandleProcessingError (the caller can do that)
// and RequestKey function. The latter value can be provided as argument by the caller.
type workflowRateLimiter struct {
	al AllowLimiter
}

func (l *workflowRateLimiter) Allow(ctx context.Context, projectID int64, eventType string) bool {
	ctx = setValues(ctx, projectID, eventType)
	return l.al.Allow(ctx)
}

type workflowRateLimiterAPI struct {
	requestCtx            context.Context //nolint:containedctx
	log                   *slog.Logger
	handleProcessingError func(msg string, err error)
}

func (r *workflowRateLimiterAPI) Log() *slog.Logger {
	return r.log
}

func (r *workflowRateLimiterAPI) HandleProcessingError(msg string, err error) {
	r.handleProcessingError(msg, err)
}

func (r *workflowRateLimiterAPI) RequestKey() []byte {
	// 0. read projectID and eventType from context values
	projectID, eventType := getValues(r.requestCtx)
	projectIDStr := strconv.FormatInt(projectID, 10)

	// 1. write key data to buffer
	var b bytes.Buffer
	b.Grow(len(projectIDStr) + 1 + len(eventType))

	b.WriteString(projectIDStr)
	b.WriteByte(':')
	b.WriteString(eventType)

	return b.Bytes()
}

func setValues(ctx context.Context, projectID int64, eventType string) context.Context {
	ctx = context.WithValue(ctx, projectIDCtxValueKey, projectID)
	ctx = context.WithValue(ctx, eventTypeCtxValueKey, eventType)
	return ctx
}

func getValues(ctx context.Context) (int64, string) {
	projectID, ok := ctx.Value(projectIDCtxValueKey).(int64)
	if !ok {
		panic("unable retrieve project ID from context values. This is a programming error")
	}

	eventType, ok := ctx.Value(eventTypeCtxValueKey).(string)
	if !ok {
		panic("Unable retrieve event type from context values. This is a programming error")
	}

	return projectID, eventType
}
