package acmeserver

import (
	"bytes"
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"golang.org/x/crypto/acme"
)

const (
	http01Challenge = "http-01"
	dns01Challenge  = "dns-01"
)

type validator interface {
	Fulfill(context.Context, *acme.Client, string, *acme.Challenge) (func(), error)
}

// ACME is a simple wrapper for an acme.Client that handles the
// certificate creation/renewal workflow using http-01 or dns-01
// challenges.
type ACME struct {
	email          string
	accountKeyPath string
	directoryURL   string
	client         *acme.Client

	handler              http.Handler
	validators           map[string]validator
	defaultChallengeType string
}

// NewACME returns a new ACME object with the provided config.
func NewACME(config *Config) (*ACME, error) {
	if config.Email == "" {
		return nil, errors.New("configuration parameter 'email' is unset")
	}
	if config.AccountKeyPath == "" {
		return nil, errors.New("configuration parameter 'account_key_path' is unset")
	}

	var h http.Handler
	v := make(map[string]validator)
	if config.HTTP.Enabled {
		httpv := newHTTPValidator()
		h = httpv
		v[http01Challenge] = httpv
	}
	if config.DNS.Enabled {
		dnsv, err := newDNSValidator(config)
		if err != nil {
			return nil, err
		}
		v[dns01Challenge] = dnsv
	}

	defaultChalType := config.DefaultChallengeType
	if defaultChalType == "" {
		defaultChalType = http01Challenge
	}
	if _, ok := v[defaultChalType]; !ok {
		return nil, fmt.Errorf("the default challenge type '%s' is not configured", defaultChalType)
	}

	directoryURL := config.DirectoryURL
	if directoryURL == "" {
		directoryURL = acme.LetsEncryptURL
	}

	return &ACME{
		email:                config.Email,
		accountKeyPath:       config.AccountKeyPath,
		directoryURL:         directoryURL,
		handler:              h,
		validators:           v,
		defaultChallengeType: defaultChalType,
	}, nil
}

// Return the account key, possibly initializing it if it does not exist.
func (a *ACME) accountKey() (crypto.Signer, error) {
	if key, err := parsePrivateKeyFromFile(a.accountKeyPath); err == nil {
		return key, err
	}

	log.Printf("generating new account key")
	eckey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	if err = encodeECDSAKey(&buf, eckey); err != nil {
		return nil, err
	}
	if err = ioutil.WriteFile(a.accountKeyPath, buf.Bytes(), 0600); err != nil {
		return nil, err
	}

	return eckey, err
}

// Return a new acme.Client, possibly initializing the account key if
// it does not exist yet. The Client is created on the first call and
// cached thereafter.
func (a *ACME) acmeClient(ctx context.Context) (*acme.Client, error) {
	if a.client != nil {
		return a.client, nil
	}

	client := &acme.Client{
		DirectoryURL: a.directoryURL,
	}
	key, err := a.accountKey()
	if err != nil {
		return nil, err
	}
	client.Key = key
	ac := &acme.Account{
		Contact: []string{"mailto:" + a.email},
	}

	// Register the account (accept TOS) if necessary. If the
	// account is already registered we get a StatusConflict,
	// which we can ignore.
	acct, err := client.Register(ctx, ac, func(_ string) bool { return true })
	if ae, ok := err.(*acme.Error); err == nil || err == acme.ErrAccountAlreadyExists || (ok && ae.StatusCode == http.StatusConflict) {
		log.Printf("ACME account %s", acct.URI)
		a.client = client
		err = nil
	}
	return a.client, err
}

// GetCertificate returns a certificate chain (and the parsed leaf
// cert), given a private key and a list of domains. The first domain
// will be the subject CN, the others will be added as subjectAltNames.
func (a *ACME) GetCertificate(ctx context.Context, key crypto.Signer, c *certConfig) (der [][]byte, leaf *x509.Certificate, err error) {
	client, err := a.acmeClient(ctx)
	if err != nil {
		return nil, nil, err
	}

	o, err := a.verifyAll(ctx, client, c)
	if err != nil {
		return nil, nil, err
	}

	csr, err := certRequest(key, c.Names)
	if err != nil {
		return nil, nil, err
	}
	der, _, err = client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
	if err != nil {
		return nil, nil, err
	}
	leaf, err = validCert(c.Names, der, key)
	if err != nil {
		return nil, nil, err
	}
	return der, leaf, nil
}

func (a *ACME) verifyAll(ctx context.Context, client *acme.Client, c *certConfig) (*acme.Order, error) {
	// Make an authorization request to the ACME server, and
	// verify that it returns a valid response with challenges.
	o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(c.Names...))
	if err != nil {
		return nil, fmt.Errorf("AuthorizeOrder failed: %v", err)
	}

	switch o.Status {
	case acme.StatusReady:
		return o, nil // already authorized
	case acme.StatusPending:
	default:
		return nil, fmt.Errorf("invalid new order status %q", o.Status)
	}

	for _, zurl := range o.AuthzURLs {
		z, err := client.GetAuthorization(ctx, zurl)
		if err != nil {
			return nil, fmt.Errorf("GetAuthorization(%s) failed: %v", zurl, err)
		}
		if z.Status != acme.StatusPending {
			continue
		}
		// Pick a challenge that matches our preferences and the
		// available validators. The validator fulfills the challenge,
		// and returns a cleanup function that we're going to call
		// before we return. All steps are sequential and idempotent.
		chal := a.pickChallenge(z.Challenges, c)
		if chal == nil {
			return nil, fmt.Errorf("unable to authorize %q", c.Names)
		}

		v, ok := a.validators[chal.Type]
		if !ok {
			return nil, fmt.Errorf("challenge type '%s' is not available", chal.Type)
		}

		for _, domain := range c.Names {
			cleanup, err := v.Fulfill(ctx, client, domain, chal)
			if err != nil {
				return nil, fmt.Errorf("fulfillment failed: %v", err)
			}
			defer cleanup()
		}

		if _, err := client.Accept(ctx, chal); err != nil {
			return nil, fmt.Errorf("challenge accept failed: %v", err)
		}
		if _, err := client.WaitAuthorization(ctx, z.URI); err != nil {
			return nil, fmt.Errorf("WaitAuthorization(%s) failed: %v", z.URI, err)
		}
	}

	// Authorizations are satisfied, wait for the CA
	// to update the order status.
	if _, err = client.WaitOrder(ctx, o.URI); err != nil {
		return nil, err
	}
	return o, nil
}

// Pick a challenge with the right type from the Challenge response
// returned by the ACME server. We have a pretty specific idea of the
// challenge type we want (either the default, or the ChallengeType
// specified in the cert config), so we just search for that one.
func (a *ACME) pickChallenge(chal []*acme.Challenge, c *certConfig) *acme.Challenge {
	ctype := a.defaultChallengeType
	if c.ChallengeType != "" {
		ctype = c.ChallengeType
	}
	for _, ch := range chal {
		if ch.Type == ctype {
			return ch
		}
	}
	return nil
}

// Handler returns the http.Handler that is used to satisfy the
// http-01 validation requests.
func (a *ACME) Handler() http.Handler {
	return a.handler
}