package acme

import (
	"errors"
	"fmt"
	mrand "math/rand/v2"
	"strings"

	"github.com/letsencrypt/boulder/core"
)

// ChallengeStrategy is an interface describing a strategy for picking
// a challenge from a given authorization.
type ChallengeStrategy interface {
	PickChallenge(*core.Authorization) (*core.Challenge, error)
}

const (
	// RandomChallengeStrategy is the name for a random challenge selection
	// strategy that will choose one of the authorization's challenges at random.
	RandomChallengeStrategy = "RANDOM"
	// The following challenge strategies will always pick the named challenge
	// type or return an error if there isn't a challenge of that type to pick.
	HTTP01ChallengeStrategy    = "HTTP-01"
	DNS01ChallengeStrategy     = "DNS-01"
	TLSALPN01ChallengeStrategy = "TLS-ALPN-01"
)

// NewChallengeStrategy returns the ChallengeStrategy for the given
// ChallengeStrategyName, or an error if it is unknown.
func NewChallengeStrategy(rawName string) (ChallengeStrategy, error) {
	var preferredType core.AcmeChallenge
	switch name := strings.ToUpper(rawName); name {
	case RandomChallengeStrategy:
		return &randomChallengeStrategy{}, nil
	case HTTP01ChallengeStrategy:
		preferredType = core.ChallengeTypeHTTP01
	case DNS01ChallengeStrategy:
		preferredType = core.ChallengeTypeDNS01
	case TLSALPN01ChallengeStrategy:
		preferredType = core.ChallengeTypeTLSALPN01
	default:
		return nil, fmt.Errorf("ChallengeStrategy %q unknown", name)
	}

	return &preferredTypeChallengeStrategy{
		preferredType: preferredType,
	}, nil
}

var (
	ErrPickChallengeNilAuthz               = errors.New("PickChallenge: provided authorization can not be nil")
	ErrPickChallengeAuthzMissingChallenges = errors.New("PickChallenge: provided authorization had no challenges")
)

// randomChallengeStrategy is a ChallengeStrategy implementation that always
// returns a random challenge from the given authorization.
type randomChallengeStrategy struct {
}

// PickChallenge for a randomChallengeStrategy returns a random challenge from
// the authorization.
func (strategy randomChallengeStrategy) PickChallenge(authz *core.Authorization) (*core.Challenge, error) {
	if authz == nil {
		return nil, ErrPickChallengeNilAuthz
	}
	if len(authz.Challenges) == 0 {
		return nil, ErrPickChallengeAuthzMissingChallenges
	}
	return &authz.Challenges[mrand.IntN(len(authz.Challenges))], nil
}

// preferredTypeChallengeStrategy is a ChallengeStrategy implementation that
// always returns the authorization's challenge with type matching the
// preferredType.
type preferredTypeChallengeStrategy struct {
	preferredType core.AcmeChallenge
}

// PickChallenge for a preferredTypeChallengeStrategy returns the authorization
// challenge that has Type equal the preferredType. An error is returned if the
// challenge doesn't have an authorization matching the preferredType.
func (strategy preferredTypeChallengeStrategy) PickChallenge(authz *core.Authorization) (*core.Challenge, error) {
	if authz == nil {
		return nil, ErrPickChallengeNilAuthz
	}
	if len(authz.Challenges) == 0 {
		return nil, ErrPickChallengeAuthzMissingChallenges
	}
	for _, chall := range authz.Challenges {
		if chall.Type == strategy.preferredType {
			return &chall, nil
		}
	}
	return nil, fmt.Errorf("authorization (ID %q) had no %q type challenge",
		authz.ID,
		strategy.preferredType)
}
