package certificates

import (
	"fmt"
	"sync"
	"time"

	"github.com/go-acme/lego/v4/lego"
	"github.com/go-acme/lego/v4/providers/dns"
	"github.com/reugn/equalizer"
	"github.com/rs/zerolog/log"

	"codeberg.org/codeberg/pages/config"
	"codeberg.org/codeberg/pages/server/cache"
)

type AcmeClient struct {
	legoClient              *lego.Client
	dnsChallengerLegoClient *lego.Client

	obtainLocks sync.Map

	acmeUseRateLimits bool

	// limiter
	acmeClientOrderLimit              *equalizer.TokenBucket
	acmeClientRequestLimit            *equalizer.TokenBucket
	acmeClientFailLimit               *equalizer.TokenBucket
	acmeClientCertificateLimitPerUser map[string]*equalizer.TokenBucket
}

func NewAcmeClient(cfg config.ACMEConfig, enableHTTPServer bool, challengeCache cache.ICache) (*AcmeClient, error) {
	acmeConfig, err := setupAcmeConfig(cfg)
	if err != nil {
		return nil, err
	}

	acmeClient, err := lego.NewClient(acmeConfig)
	if err != nil {
		log.Fatal().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
	} else {
		err = acmeClient.Challenge.SetTLSALPN01Provider(AcmeTLSChallengeProvider{challengeCache})
		if err != nil {
			log.Error().Err(err).Msg("Can't create TLS-ALPN-01 provider")
		}
		if enableHTTPServer {
			err = acmeClient.Challenge.SetHTTP01Provider(AcmeHTTPChallengeProvider{challengeCache})
			if err != nil {
				log.Error().Err(err).Msg("Can't create HTTP-01 provider")
			}
		}
	}

	mainDomainAcmeClient, err := lego.NewClient(acmeConfig)
	if err != nil {
		log.Error().Err(err).Msg("Can't create ACME client, continuing with mock certs only")
	} else {
		if cfg.DNSProvider == "" {
			// using mock wildcard certs
			mainDomainAcmeClient = nil
		} else {
			// use DNS-Challenge https://go-acme.github.io/lego/dns/
			provider, err := dns.NewDNSChallengeProviderByName(cfg.DNSProvider)
			if err != nil {
				return nil, fmt.Errorf("can not create DNS Challenge provider: %w", err)
			}
			if err := mainDomainAcmeClient.Challenge.SetDNS01Provider(provider); err != nil {
				return nil, fmt.Errorf("can not create DNS-01 provider: %w", err)
			}
		}
	}

	return &AcmeClient{
		legoClient:              acmeClient,
		dnsChallengerLegoClient: mainDomainAcmeClient,

		acmeUseRateLimits: cfg.UseRateLimits,

		obtainLocks: sync.Map{},

		// limiter

		// rate limit is 300 / 3 hours, we want 200 / 2 hours but to refill more often, so that's 25 new domains every 15 minutes
		// TODO: when this is used a lot, we probably have to think of a somewhat better solution?
		acmeClientOrderLimit: equalizer.NewTokenBucket(25, 15*time.Minute),
		// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
		acmeClientRequestLimit: equalizer.NewTokenBucket(5, 1*time.Second),
		// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/
		acmeClientFailLimit: equalizer.NewTokenBucket(5, 1*time.Hour),
		// checkUserLimit() use this to rate also per user
		acmeClientCertificateLimitPerUser: map[string]*equalizer.TokenBucket{},
	}, nil
}