// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package certmagic

import (
	"crypto/tls"
	"fmt"
	"sync"
	"time"

	"github.com/go-acme/lego/certcrypto"
	"github.com/go-acme/lego/certificate"
	"github.com/go-acme/lego/challenge"
	"github.com/go-acme/lego/challenge/tlsalpn01"
	"github.com/go-acme/lego/lego"
)

// Config configures a certificate manager instance.
// An empty Config is not valid: use New() to obtain
// a valid Config.
type Config struct {
	// The endpoint of the directory for the ACME
	// CA we are to use
	CA string

	// The email address to use when creating or
	// selecting an existing ACME server account
	Email string

	// Set to true if agreed to the CA's
	// subscriber agreement
	Agreed bool

	// Disable all HTTP challenges
	DisableHTTPChallenge bool

	// Disable all TLS-ALPN challenges
	DisableTLSALPNChallenge bool

	// How long before expiration to renew certificates
	RenewDurationBefore time.Duration

	// How long before expiration to require a renewed
	// certificate when in interactive mode, like when
	// the program is first starting up (see
	// mholt/caddy#1680). A wider window between
	// RenewDurationBefore and this value will suppress
	// errors under duress (bad) but hopefully this duration
	// will give it enough time for the blockage to be
	// relieved.
	RenewDurationBeforeAtStartup time.Duration

	// An optional event callback clients can set
	// to subscribe to certain things happening
	// internally by this config; invocations are
	// synchronous, so make them return quickly!
	OnEvent func(event string, data interface{})

	// The host (ONLY the host, not port) to listen
	// on if necessary to start a listener to solve
	// an ACME challenge
	ListenHost string

	// The alternate port to use for the ACME HTTP
	// challenge; if non-empty, this port will be
	// used instead of HTTPChallengePort to spin up
	// a listener for the HTTP challenge
	AltHTTPPort int

	// The alternate port to use for the ACME
	// TLS-ALPN challenge; the system must forward
	// TLSALPNChallengePort to this port for
	// challenge to succeed
	AltTLSALPNPort int

	// The DNS provider to use when solving the
	// ACME DNS challenge
	DNSProvider challenge.Provider

	// The type of key to use when generating
	// certificates
	KeyType certcrypto.KeyType

	// The maximum amount of time to allow for
	// obtaining a certificate. If empty, the
	// default from the underlying lego lib is
	// used. If set, it must not be too low so
	// as to cancel orders too early, running
	// the risk of rate limiting.
	CertObtainTimeout time.Duration

	// DefaultServerName specifies a server name
	// to use when choosing a certificate if the
	// ClientHello's ServerName field is empty
	DefaultServerName string

	// The state needed to operate on-demand TLS
	OnDemand *OnDemandConfig

	// Add the must staple TLS extension to the
	// CSR generated by lego/acme
	MustStaple bool

	// The storage to access when storing or
	// loading TLS assets
	Storage Storage

	// NewManager returns a new Manager. If nil,
	// an ACME client will be created and used.
	NewManager func(interactive bool) (Manager, error)

	// Pointer to the in-memory certificate cache
	certCache *Cache

	// Map of client config key to ACME clients
	// so they can be reused
	// TODO: It might be better if these were globally cached, rather than per-config, which are ephemeral... but maybe evict them after a certain time, like 1 day or something
	acmeClients   map[string]*lego.Client
	acmeClientsMu *sync.Mutex
}

// NewDefault makes a valid config based on the package
// Default config. Most users will call this function
// instead of New() since most use cases require only a
// single config for any and all certificates.
//
// If your requirements are more advanced (for example,
// multiple configs depending on the certificate), then use
// New() instead. (You will need to make your own Cache
// first.) If you only need a single Config to manage your
// certs (even if that config changes, as long as it is the
// only one), customize the Default package variable before
// calling NewDefault().
//
// All calls to NewDefault() will return configs that use the
// same, default certificate cache. All configs returned
// by NewDefault() are based on the values of the fields of
// Default at the time it is called.
func NewDefault() *Config {
	defaultCacheMu.Lock()
	if defaultCache == nil {
		defaultCache = NewCache(CacheOptions{
			// the cache will likely need to renew certificates,
			// so it will need to know how to do that, which
			// depends on the certificate being managed and which
			// can change during the lifetime of the cache; this
			// callback makes it possible to get the latest and
			// correct config with which to manage the cert,
			// but if the user does not provide one, we can only
			// assume that we are to use the default config
			GetConfigForCert: func(Certificate) (Config, error) {
				return Default, nil
			},
		})
	}
	certCache := defaultCache
	defaultCacheMu.Unlock()

	return newWithCache(certCache, Default)
}

// New makes a new, valid config based on cfg and
// uses the provided certificate cache. certCache
// MUST NOT be nil or this function will panic.
//
// Use this method when you have an advanced use case
// that requires a custom certificate cache and config
// that may differ from the Default. For example, if
// not all certificates are managed/renewed the same
// way, you need to make your own Cache value with a
// GetConfigForCert callback that returns the correct
// configuration for each certificate. However, for
// the vast majority of cases, there will be only a
// single Config, thus the default cache (which always
// uses the default Config) and default config will
// suffice, and you should use New() instead.
func New(certCache *Cache, cfg Config) *Config {
	if certCache == nil {
		panic("a certificate cache is required")
	}
	if certCache.options.GetConfigForCert == nil {
		panic("cache must have GetConfigForCert set in its options")
	}
	return newWithCache(certCache, cfg)
}

// newWithCache ensures that cfg is a valid config by populating
// zero-value fields from the Default Config. If certCache is
// nil, this function panics.
func newWithCache(certCache *Cache, cfg Config) *Config {
	if certCache == nil {
		panic("cannot make a valid config without a pointer to a certificate cache")
	}

	// fill in default values
	if cfg.CA == "" {
		cfg.CA = Default.CA
	}
	if cfg.Email == "" {
		cfg.Email = Default.Email
	}
	if cfg.OnDemand == nil {
		cfg.OnDemand = Default.OnDemand
	}
	if !cfg.Agreed {
		cfg.Agreed = Default.Agreed
	}
	if !cfg.DisableHTTPChallenge {
		cfg.DisableHTTPChallenge = Default.DisableHTTPChallenge
	}
	if !cfg.DisableTLSALPNChallenge {
		cfg.DisableTLSALPNChallenge = Default.DisableTLSALPNChallenge
	}
	if cfg.RenewDurationBefore == 0 {
		cfg.RenewDurationBefore = Default.RenewDurationBefore
	}
	if cfg.RenewDurationBeforeAtStartup == 0 {
		cfg.RenewDurationBeforeAtStartup = Default.RenewDurationBeforeAtStartup
	}
	if cfg.OnEvent == nil {
		cfg.OnEvent = Default.OnEvent
	}
	if cfg.ListenHost == "" {
		cfg.ListenHost = Default.ListenHost
	}
	if cfg.AltHTTPPort == 0 {
		cfg.AltHTTPPort = Default.AltHTTPPort
	}
	if cfg.AltTLSALPNPort == 0 {
		cfg.AltTLSALPNPort = Default.AltTLSALPNPort
	}
	if cfg.DNSProvider == nil {
		cfg.DNSProvider = Default.DNSProvider
	}
	if cfg.KeyType == "" {
		cfg.KeyType = Default.KeyType
	}
	if cfg.CertObtainTimeout == 0 {
		cfg.CertObtainTimeout = Default.CertObtainTimeout
	}
	if cfg.DefaultServerName == "" {
		cfg.DefaultServerName = Default.DefaultServerName
	}
	if cfg.OnDemand == nil {
		cfg.OnDemand = Default.OnDemand
	}
	if !cfg.MustStaple {
		cfg.MustStaple = Default.MustStaple
	}
	if cfg.Storage == nil {
		cfg.Storage = Default.Storage
	}
	if cfg.NewManager == nil {
		cfg.NewManager = Default.NewManager
	}

	// absolutely don't allow a nil storage,
	// because that would make almost anything
	// a config can do pointless
	if cfg.Storage == nil {
		cfg.Storage = defaultFileStorage
	}

	// ensure the unexported fields are valid
	cfg.certCache = certCache
	cfg.acmeClients = make(map[string]*lego.Client)
	cfg.acmeClientsMu = new(sync.Mutex)

	return &cfg
}

// Manage causes the certificates for domainNames to be managed
// according to cfg. If cfg is enabled for OnDemand, then this
// simply whitelists the domain names. Otherwise, the certificate(s)
// for each name are loaded from storage or obtained from the CA;
// and if loaded from storage, renewed if they are expiring or
// expired. It then caches the certificate in memory and is
// prepared to serve them up during TLS handshakes.
func (cfg *Config) Manage(domainNames []string) error {
	for _, domainName := range domainNames {
		if !HostQualifies(domainName) {
			return fmt.Errorf("name does not qualify for automatic certificate management: %s", domainName)
		}

		// if on-demand is configured, simply whitelist this name
		if cfg.OnDemand != nil {
			if !cfg.OnDemand.whitelistContains(domainName) {
				cfg.OnDemand.HostWhitelist = append(cfg.OnDemand.HostWhitelist, domainName)
			}
			continue
		}

		// try loading an existing certificate; if it doesn't
		// exist yet, obtain one and try loading it again
		cert, err := cfg.CacheManagedCertificate(domainName)
		if err != nil {
			if _, ok := err.(ErrNotExist); ok {
				// if it doesn't exist, get it, then try loading it again
				err := cfg.ObtainCert(domainName, false)
				if err != nil {
					return fmt.Errorf("%s: obtaining certificate: %v", domainName, err)
				}
				cert, err = cfg.CacheManagedCertificate(domainName)
				if err != nil {
					return fmt.Errorf("%s: caching certificate after obtaining it: %v", domainName, err)
				}
				continue
			}
			return fmt.Errorf("%s: caching certificate: %v", domainName, err)
		}

		// for existing certificates, make sure it is renewed
		if cert.NeedsRenewal(cfg) {
			err := cfg.RenewCert(domainName, false)
			if err != nil {
				return fmt.Errorf("%s: renewing certificate: %v", domainName, err)
			}
		}
	}

	return nil
}

// ObtainCert obtains a certificate for name using cfg, as long
// as a certificate does not already exist in storage for that
// name. The name must qualify and cfg must be flagged as Managed.
// This function is a no-op if storage already has a certificate
// for name.
//
// It only obtains and stores certificates (and their keys),
// it does not load them into memory. If interactive is true,
// the user may be shown a prompt.
func (cfg *Config) ObtainCert(name string, interactive bool) error {
	if cfg.storageHasCertResources(name) {
		return nil
	}
	skip, err := cfg.preObtainOrRenewChecks(name, interactive)
	if err != nil {
		return err
	}
	if skip {
		return nil
	}
	manager, err := cfg.newManager(interactive)
	if err != nil {
		return err
	}
	return manager.Obtain(name)
}

// RenewCert renews the certificate for name using cfg. It stows the
// renewed certificate and its assets in storage if successful.
func (cfg *Config) RenewCert(name string, interactive bool) error {
	skip, err := cfg.preObtainOrRenewChecks(name, interactive)
	if err != nil {
		return err
	}
	if skip {
		return nil
	}
	manager, err := cfg.newManager(interactive)
	if err != nil {
		return err
	}
	return manager.Renew(name)
}

// RevokeCert revokes the certificate for domain via ACME protocol.
func (cfg *Config) RevokeCert(domain string, interactive bool) error {
	manager, err := cfg.newManager(interactive)
	if err != nil {
		return err
	}
	return manager.Revoke(domain)
}

// TLSConfig is an opinionated method that returns a
// recommended, modern TLS configuration that can be
// used to configure TLS listeners, which also supports
// the TLS-ALPN challenge and serves up certificates
// managed by cfg.
//
// Unlike the package TLS() function, this method does
// not, by itself, enable certificate management for
// any domain names.
//
// Feel free to further customize the returned tls.Config,
// but do not mess with the GetCertificate or NextProtos
// fields unless you know what you're doing, as they're
// necessary to solve the TLS-ALPN challenge.
func (cfg *Config) TLSConfig() *tls.Config {
	return &tls.Config{
		// these two fields necessary for TLS-ALPN challenge
		GetCertificate: cfg.GetCertificate,
		NextProtos:     []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol},

		// the rest recommended for modern TLS servers
		MinVersion: tls.VersionTLS12,
		CurvePreferences: []tls.CurveID{
			tls.X25519,
			tls.CurveP256,
		},
		CipherSuites:             preferredDefaultCipherSuites(),
		PreferServerCipherSuites: true,
	}
}

// preObtainOrRenewChecks perform a few simple checks before
// obtaining or renewing a certificate with ACME, and returns
// whether this name should be skipped (like if it's not
// managed TLS) as well as any error. It ensures that the
// config is Managed, that the name qualifies for a certificate,
// and that an email address is available.
func (cfg *Config) preObtainOrRenewChecks(name string, allowPrompts bool) (bool, error) {
	if !HostQualifies(name) {
		return true, nil
	}

	err := cfg.getEmail(allowPrompts)
	if err != nil {
		return false, err
	}

	return false, nil
}

// storageHasCertResources returns true if the storage
// associated with cfg's certificate cache has all the
// resources related to the certificate for domain: the
// certificate, the private key, and the metadata.
func (cfg *Config) storageHasCertResources(domain string) bool {
	certKey := StorageKeys.SiteCert(cfg.CA, domain)
	keyKey := StorageKeys.SitePrivateKey(cfg.CA, domain)
	metaKey := StorageKeys.SiteMeta(cfg.CA, domain)
	return cfg.Storage.Exists(certKey) &&
		cfg.Storage.Exists(keyKey) &&
		cfg.Storage.Exists(metaKey)
}

// managedCertNeedsRenewal returns true if certRes is
// expiring soon or already expired, or if the process
// of checking the expiration returned an error.
func (cfg *Config) managedCertNeedsRenewal(certRes certificate.Resource) bool {
	cert, err := makeCertificate(certRes.Certificate, certRes.PrivateKey)
	if err != nil {
		return true
	}
	return cert.NeedsRenewal(cfg)
}

// Manager is a type that can manage a certificate.
// They are usually very short-lived.
type Manager interface {
	Obtain(name string) error
	Renew(name string) error
	Revoke(name string) error
}
