package acme

import (
	"context"
	"net"
	"net/http"
	"time"

	"github.com/cenkalti/backoff/v4"
	"github.com/containous/traefik/v2/pkg/log"
	"github.com/containous/traefik/v2/pkg/safe"
	"github.com/go-acme/lego/v3/challenge"
	"github.com/go-acme/lego/v3/challenge/http01"
	"github.com/gorilla/mux"
)

var _ challenge.ProviderTimeout = (*challengeHTTP)(nil)

type challengeHTTP struct {
	Store ChallengeStore
}

// Present presents a challenge to obtain new ACME certificate.
func (c *challengeHTTP) Present(domain, token, keyAuth string) error {
	return c.Store.SetHTTPChallengeToken(token, domain, []byte(keyAuth))
}

// CleanUp cleans the challenges when certificate is obtained.
func (c *challengeHTTP) CleanUp(domain, token, keyAuth string) error {
	return c.Store.RemoveHTTPChallengeToken(token, domain)
}

// Timeout calculates the maximum of time allowed to resolved an ACME challenge.
func (c *challengeHTTP) Timeout() (timeout, interval time.Duration) {
	return 60 * time.Second, 5 * time.Second
}

// CreateHandler creates a HTTP handler to expose the token for the HTTP challenge.
func (p *Provider) CreateHandler(notFoundHandler http.Handler) http.Handler {
	router := mux.NewRouter().SkipClean(true)
	router.NotFoundHandler = notFoundHandler

	router.Methods(http.MethodGet).
		Path(http01.ChallengePath("{token}")).
		Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
			vars := mux.Vars(req)

			ctx := log.With(context.Background(), log.Str(log.ProviderName, p.ResolverName+".acme"))
			logger := log.FromContext(ctx)

			if token, ok := vars["token"]; ok {
				domain, _, err := net.SplitHostPort(req.Host)
				if err != nil {
					logger.Debugf("Unable to split host and port: %v. Fallback to request host.", err)
					domain = req.Host
				}

				tokenValue := getTokenValue(ctx, token, domain, p.ChallengeStore)
				if len(tokenValue) > 0 {
					rw.WriteHeader(http.StatusOK)
					_, err = rw.Write(tokenValue)
					if err != nil {
						logger.Errorf("Unable to write token: %v", err)
					}
					return
				}
			}
			rw.WriteHeader(http.StatusNotFound)
		}))

	return router
}

func getTokenValue(ctx context.Context, token, domain string, store ChallengeStore) []byte {
	logger := log.FromContext(ctx)
	logger.Debugf("Retrieving the ACME challenge for token %v...", token)

	var result []byte

	operation := func() error {
		var err error
		result, err = store.GetHTTPChallengeToken(token, domain)
		return err
	}

	notify := func(err error, time time.Duration) {
		logger.Errorf("Error getting challenge for token retrying in %s", time)
	}

	ebo := backoff.NewExponentialBackOff()
	ebo.MaxElapsedTime = 60 * time.Second
	err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
	if err != nil {
		logger.Errorf("Cannot retrieve the ACME challenge for token %v: %v", token, err)
		return []byte{}
	}

	return result
}
