// Package lightsail implements a DNS provider for solving the DNS-01 challenge
// using AWS Lightsail DNS.
package lightsail

import (
	"math/rand"
	"os"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/lightsail"
	"github.com/xenolf/lego/acme"
)

const (
	maxRetries = 5
)

// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
	client  *lightsail.Lightsail
	dnsZone string
}

// customRetryer implements the client.Retryer interface by composing the
// DefaultRetryer. It controls the logic for retrying recoverable request
// errors (e.g. when rate limits are exceeded).
type customRetryer struct {
	client.DefaultRetryer
}

// RetryRules overwrites the DefaultRetryer's method.
// It uses a basic exponential backoff algorithm that returns an initial
// delay of ~400ms with an upper limit of ~30 seconds which should prevent
// causing a high number of consecutive throttling errors.
// For reference: Route 53 enforces an account-wide(!) 5req/s query limit.
func (c customRetryer) RetryRules(r *request.Request) time.Duration {
	retryCount := r.RetryCount
	if retryCount > 7 {
		retryCount = 7
	}

	delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200)
	return time.Duration(delay) * time.Millisecond
}

// NewDNSProvider returns a DNSProvider instance configured for the AWS
// Lightsail service.
//
// AWS Credentials are automatically detected in the following locations
// and prioritized in the following order:
// 1. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
//     [AWS_SESSION_TOKEN], [DNS_ZONE]
// 2. Shared credentials file (defaults to ~/.aws/credentials)
// 3. Amazon EC2 IAM role
//
// public hosted zone via the FQDN.
//
// See also: https://github.com/aws/aws-sdk-go/wiki/configuring-sdk
func NewDNSProvider() (*DNSProvider, error) {
	r := customRetryer{}
	r.NumMaxRetries = maxRetries

	config := aws.NewConfig().WithRegion("us-east-1")
	sess, err := session.NewSession(request.WithRetryer(config, r))
	if err != nil {
		return nil, err
	}

	return &DNSProvider{
		dnsZone: os.Getenv("DNS_ZONE"),
		client:  lightsail.New(sess),
	}, nil
}

// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
	fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
	value = `"` + value + `"`

	err := d.newTxtRecord(domain, fqdn, value)
	return err
}

// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
	fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
	value = `"` + value + `"`
	params := &lightsail.DeleteDomainEntryInput{
		DomainName: aws.String(d.dnsZone),
		DomainEntry: &lightsail.DomainEntry{
			Name:   aws.String(fqdn),
			Type:   aws.String("TXT"),
			Target: aws.String(value),
		},
	}
	_, err := d.client.DeleteDomainEntry(params)
	return err
}

func (d *DNSProvider) newTxtRecord(domain string, fqdn string, value string) error {
	params := &lightsail.CreateDomainEntryInput{
		DomainName: aws.String(d.dnsZone),
		DomainEntry: &lightsail.DomainEntry{
			Name:   aws.String(fqdn),
			Target: aws.String(value),
			Type:   aws.String("TXT"),
		},
	}
	_, err := d.client.CreateDomainEntry(params)
	return err
}