package caddytls

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	weakrand "math/rand/v2"
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/caddyserver/certmagic"
	"github.com/cloudflare/circl/hpke"
	"github.com/cloudflare/circl/kem"
	"github.com/libdns/libdns"
	"go.uber.org/zap"
	"golang.org/x/crypto/cryptobyte"

	"github.com/caddyserver/caddy/v2"
)

func init() {
	caddy.RegisterModule(ECHDNSPublisher{})
}

// ECH enables Encrypted ClientHello (ECH) and configures its management.
//
// Note that, as of Caddy 2.10 (~March 2025), ECH keys are not automatically
// rotated due to a limitation in the Go standard library (see
// https://github.com/golang/go/issues/71920). This should be resolved when
// Go 1.25 is released (~Aug. 2025), and Caddy will be updated to automatically
// rotate ECH keys/configs at that point.
//
// EXPERIMENTAL: Subject to change.
type ECH struct {
	// The list of ECH configurations for which to automatically generate
	// and rotate keys. At least one is required to enable ECH.
	Configs []ECHConfiguration `json:"configs,omitempty"`

	// Publication describes ways to publish ECH configs for clients to
	// discover and use. Without publication, most clients will not use
	// ECH at all, and those that do will suffer degraded performance.
	//
	// Most major browsers support ECH by way of publication to HTTPS
	// DNS RRs. (This also typically requires that they use DoH or DoT.)
	Publication []*ECHPublication `json:"publication,omitempty"`

	// map of public_name to list of configs
	configs map[string][]echConfig
}

// Provision loads or creates ECH configs and returns outer names (for certificate
// management), but does not publish any ECH configs. The DNS module is used as
// a default for later publishing if needed.
func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
	logger := ctx.Logger().Named("ech")

	// set up publication modules before we need to obtain a lock in storage,
	// since this is strictly internal and doesn't require synchronization
	for i, pub := range ech.Publication {
		mods, err := ctx.LoadModule(pub, "PublishersRaw")
		if err != nil {
			return nil, fmt.Errorf("loading ECH publication modules: %v", err)
		}
		for _, modIface := range mods.(map[string]any) {
			ech.Publication[i].publishers = append(ech.Publication[i].publishers, modIface.(ECHPublisher))
		}
	}

	// the rest of provisioning needs an exclusive lock so that instances aren't
	// stepping on each other when setting up ECH configs
	storage := ctx.Storage()
	const echLockName = "ech_provision"
	if err := storage.Lock(ctx, echLockName); err != nil {
		return nil, err
	}
	defer func() {
		if err := storage.Unlock(ctx, echLockName); err != nil {
			logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err))
		}
	}()

	var outerNames []string //nolint:prealloc // (FALSE POSITIVE - see https://github.com/alexkohler/prealloc/issues/30)

	// start by loading all the existing configs (even the older ones on the way out,
	// since some clients may still be using them if they haven't yet picked up on the
	// new configs)
	cfgKeys, err := storage.List(ctx, echConfigsKey, false)
	if err != nil && !errors.Is(err, fs.ErrNotExist) { // OK if dir doesn't exist; it will be created
		return nil, err
	}
	for _, cfgKey := range cfgKeys {
		cfg, err := loadECHConfig(ctx, path.Base(cfgKey))
		if err != nil {
			return nil, err
		}
		// if any part of the config's folder was corrupted, the load function will
		// clean it up and not return an error, since configs are immutable and
		// fairly ephemeral... so just check that we actually got a populated config
		if cfg.configBin == nil || cfg.privKeyBin == nil {
			continue
		}
		logger.Debug("loaded ECH config",
			zap.String("public_name", cfg.RawPublicName),
			zap.Uint8("id", cfg.ConfigID))
		ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg)
		outerNames = append(outerNames, cfg.RawPublicName)
	}

	// all existing configs are now loaded; see if we need to make any new ones
	// based on the input configuration, and also mark the most recent one(s) as
	// current/active, so they can be used for ECH retries

	for _, cfg := range ech.Configs {
		publicName := strings.ToLower(strings.TrimSpace(cfg.OuterSNI))

		if list, ok := ech.configs[publicName]; ok && len(list) > 0 {
			// at least one config with this public name was loaded, so find the
			// most recent one and mark it as active to be used with retries
			var mostRecentDate time.Time
			var mostRecentIdx int
			for i, c := range list {
				if mostRecentDate.IsZero() || c.meta.Created.After(mostRecentDate) {
					mostRecentDate = c.meta.Created
					mostRecentIdx = i
				}
			}
			list[mostRecentIdx].sendAsRetry = true
		} else {
			// no config with this public name was loaded, so create one
			echCfg, err := generateAndStoreECHConfig(ctx, publicName)
			if err != nil {
				return nil, err
			}
			logger.Debug("generated new ECH config",
				zap.String("public_name", echCfg.RawPublicName),
				zap.Uint8("id", echCfg.ConfigID))
			ech.configs[publicName] = append(ech.configs[publicName], echCfg)
			outerNames = append(outerNames, publicName)
		}
	}

	return outerNames, nil
}

func (t *TLS) publishECHConfigs() error {
	logger := t.logger.Named("ech")

	// make publication exclusive, since we don't need to repeat this unnecessarily
	storage := t.ctx.Storage()
	const echLockName = "ech_publish"
	if err := storage.Lock(t.ctx, echLockName); err != nil {
		return err
	}
	defer func() {
		if err := storage.Unlock(t.ctx, echLockName); err != nil {
			logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err))
		}
	}()

	// get the publication config, or use a default if not specified
	// (the default publication config should be to publish all ECH
	// configs to the app-global DNS provider; if no DNS provider is
	// configured, then this whole function is basically a no-op)
	publicationList := t.EncryptedClientHello.Publication
	if publicationList == nil {
		if dnsProv, ok := t.dns.(ECHDNSProvider); ok {
			publicationList = []*ECHPublication{
				{
					publishers: []ECHPublisher{
						&ECHDNSPublisher{
							provider: dnsProv,
							logger:   t.logger,
						},
					},
				},
			}
		}
	}

	// for each publication config, build the list of ECH configs to
	// publish with it, and figure out which inner names to publish
	// to/for, then publish
	for _, publication := range publicationList {
		// this publication is either configured for specific ECH configs,
		// or we just use an implied default of all ECH configs
		var echCfgList echConfigList
		var configIDs []uint8 // TODO: use IDs or the outer names?
		if publication.Configs == nil {
			// by default, publish all configs
			for _, configs := range t.EncryptedClientHello.configs {
				echCfgList = append(echCfgList, configs...)
				for _, c := range configs {
					configIDs = append(configIDs, c.ConfigID)
				}
			}
		} else {
			for _, cfgOuterName := range publication.Configs {
				if cfgList, ok := t.EncryptedClientHello.configs[cfgOuterName]; ok {
					echCfgList = append(echCfgList, cfgList...)
					for _, c := range cfgList {
						configIDs = append(configIDs, c.ConfigID)
					}
				}
			}
		}

		// marshal the ECH config list as binary for publication
		echCfgListBin, err := echCfgList.MarshalBinary()
		if err != nil {
			return fmt.Errorf("marshaling ECH config list: %v", err)
		}

		// now we have our list of ECH configs to publish and the inner names
		// to publish for (i.e. the names being protected); iterate each publisher
		// and do the publish for any config+name that needs a publish
		for _, publisher := range publication.publishers {
			publisherKey := publisher.PublisherKey()

			// by default, publish for all (non-outer) server names, unless
			// a specific list of names is configured
			var serverNamesSet map[string]struct{}
			if publication.Domains == nil {
				serverNamesSet = make(map[string]struct{}, len(t.serverNames))
				for name := range t.serverNames {
					serverNamesSet[name] = struct{}{}
				}
			} else {
				serverNamesSet = make(map[string]struct{}, len(publication.Domains))
				for _, name := range publication.Domains {
					serverNamesSet[name] = struct{}{}
				}
			}

			// remove any domains from the set which have already had all configs in the
			// list published by this publisher, to avoid always re-publishing unnecessarily
			for configuredInnerName := range serverNamesSet {
				allConfigsPublished := true
				for _, cfg := range echCfgList {
					// TODO: Potentially utilize the timestamp (map value) for recent-enough publication, instead of just checking for existence
					if _, ok := cfg.meta.Publications[publisherKey][configuredInnerName]; !ok {
						allConfigsPublished = false
						break
					}
				}
				if allConfigsPublished {
					delete(serverNamesSet, configuredInnerName)
				}
			}

			// if all the (inner) domains have had this ECH config list published
			// by this publisher, then try the next publication config
			if len(serverNamesSet) == 0 {
				logger.Debug("ECH config list already published by publisher for associated domains",
					zap.Uint8s("config_ids", configIDs),
					zap.String("publisher", publisherKey))
				continue
			}

			// convert the set of names to a slice
			dnsNamesToPublish := make([]string, 0, len(serverNamesSet))
			for name := range serverNamesSet {
				dnsNamesToPublish = append(dnsNamesToPublish, name)
			}

			logger.Debug("publishing ECH config list",
				zap.Strings("domains", dnsNamesToPublish),
				zap.Uint8s("config_ids", configIDs))

			// publish this ECH config list with this publisher
			pubTime := time.Now()
			err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin)
			if err != nil {
				t.logger.Error("publishing ECH configuration list",
					zap.Strings("for_domains", publication.Domains),
					zap.Error(err))
			}

			// update publication history, so that we don't unnecessarily republish every time
			for _, cfg := range echCfgList {
				if cfg.meta.Publications == nil {
					cfg.meta.Publications = make(publicationHistory)
				}
				if _, ok := cfg.meta.Publications[publisherKey]; !ok {
					cfg.meta.Publications[publisherKey] = make(map[string]time.Time)
				}
				for _, name := range dnsNamesToPublish {
					cfg.meta.Publications[publisherKey][name] = pubTime
				}
				metaBytes, err := json.Marshal(cfg.meta)
				if err != nil {
					return fmt.Errorf("marshaling ECH config metadata: %v", err)
				}
				metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json")
				if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil {
					return fmt.Errorf("storing updated ECH config metadata: %v", err)
				}
			}
		}
	}

	return nil
}

// loadECHConfig loads the config from storage with the given configID.
// An error is not actually returned in some cases the config fails to
// load because in some cases it just means the config ID folder has
// been cleaned up in storage, maybe due to an incomplete set of keys
// or corrupted contents; in any case, the only rectification is to
// delete it and make new keys (an error IS returned if deleting the
// corrupted keys fails, for example). Check the returned echConfig for
// non-nil privKeyBin and configBin values before using.
func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) {
	storage := ctx.Storage()
	logger := ctx.Logger()

	cfgIDKey := path.Join(echConfigsKey, configID)
	keyKey := path.Join(cfgIDKey, "key.bin")
	configKey := path.Join(cfgIDKey, "config.bin")
	metaKey := path.Join(cfgIDKey, "meta.json")

	// if loading anything fails, might as well delete this folder and free up
	// the config ID; spec is designed to rotate configs frequently anyway
	// (I consider it a more serious error if we can't clean up the folder,
	// since leaving stray storage keys is confusing)
	privKeyBytes, err := storage.Load(ctx, keyKey)
	if err != nil {
		delErr := storage.Delete(ctx, cfgIDKey)
		if delErr != nil {
			return echConfig{}, fmt.Errorf("error loading private key (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
		}
		logger.Warn("could not load ECH private key; deleting its config folder",
			zap.String("config_id", configID),
			zap.Error(err))
		return echConfig{}, nil
	}
	echConfigBytes, err := storage.Load(ctx, configKey)
	if err != nil {
		delErr := storage.Delete(ctx, cfgIDKey)
		if delErr != nil {
			return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
		}
		logger.Warn("could not load ECH config; deleting its config folder",
			zap.String("config_id", configID),
			zap.Error(err))
		return echConfig{}, nil
	}
	var cfg echConfig
	if err := cfg.UnmarshalBinary(echConfigBytes); err != nil {
		delErr := storage.Delete(ctx, cfgIDKey)
		if delErr != nil {
			return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
		}
		logger.Warn("could not load ECH config; deleted its config folder",
			zap.String("config_id", configID),
			zap.Error(err))
		return echConfig{}, nil
	}
	metaBytes, err := storage.Load(ctx, metaKey)
	if err != nil {
		delErr := storage.Delete(ctx, cfgIDKey)
		if delErr != nil {
			return echConfig{}, fmt.Errorf("error loading ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
		}
		logger.Warn("could not load ECH metadata; deleted its config folder",
			zap.String("config_id", configID),
			zap.Error(err))
		return echConfig{}, nil
	}
	var meta echConfigMeta
	if err := json.Unmarshal(metaBytes, &meta); err != nil {
		// even though it's just metadata, reset the whole config since we can't reliably maintain it
		delErr := storage.Delete(ctx, cfgIDKey)
		if delErr != nil {
			return echConfig{}, fmt.Errorf("error decoding ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
		}
		logger.Warn("could not JSON-decode ECH metadata; deleted its config folder",
			zap.String("config_id", configID),
			zap.Error(err))
		return echConfig{}, nil
	}

	cfg.privKeyBin = privKeyBytes
	cfg.configBin = echConfigBytes
	cfg.meta = meta

	return cfg, nil
}

func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, error) {
	// Go currently has very strict requirements for server-side ECH configs,
	// to quote the Go 1.24 godoc (with typos of AEAD IDs corrected):
	//
	// "Config should be a marshalled ECHConfig associated with PrivateKey. This
	// must match the config provided to clients byte-for-byte. The config
	// should only specify the DHKEM(X25519, HKDF-SHA256) KEM ID (0x0020), the
	// HKDF-SHA256 KDF ID (0x0001), and a subset of the following AEAD IDs:
	// AES-128-GCM (0x0001), AES-256-GCM (0x0002), ChaCha20Poly1305 (0x0003)."
	//
	// So we need to be sure we generate a config within these parameters
	// so the Go TLS server can use it.

	// generate a key pair
	const kemChoice = hpke.KEM_X25519_HKDF_SHA256
	publicKey, privateKey, err := kemChoice.Scheme().GenerateKeyPair()
	if err != nil {
		return echConfig{}, err
	}

	// find an available config ID
	configID, err := newECHConfigID(ctx)
	if err != nil {
		return echConfig{}, fmt.Errorf("generating unique config ID: %v", err)
	}

	echCfg := echConfig{
		PublicKey:     publicKey,
		Version:       draftTLSESNI22,
		ConfigID:      configID,
		RawPublicName: publicName,
		KEMID:         kemChoice,
		CipherSuites: []hpkeSymmetricCipherSuite{
			{
				KDFID:  hpke.KDF_HKDF_SHA256,
				AEADID: hpke.AEAD_AES128GCM,
			},
			{
				KDFID:  hpke.KDF_HKDF_SHA256,
				AEADID: hpke.AEAD_AES256GCM,
			},
			{
				KDFID:  hpke.KDF_HKDF_SHA256,
				AEADID: hpke.AEAD_ChaCha20Poly1305,
			},
		},
		sendAsRetry: true,
	}
	meta := echConfigMeta{
		Created: time.Now(),
	}

	privKeyBytes, err := privateKey.MarshalBinary()
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH private key: %v", err)
	}
	echConfigBytes, err := echCfg.MarshalBinary()
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH config: %v", err)
	}
	metaBytes, err := json.Marshal(meta)
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH config metadata: %v", err)
	}

	parentKey := path.Join(echConfigsKey, strconv.Itoa(int(configID)))
	keyKey := path.Join(parentKey, "key.bin")
	configKey := path.Join(parentKey, "config.bin")
	metaKey := path.Join(parentKey, "meta.json")

	if err := ctx.Storage().Store(ctx, keyKey, privKeyBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH private key: %v", err)
	}
	if err := ctx.Storage().Store(ctx, configKey, echConfigBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH config: %v", err)
	}
	if err := ctx.Storage().Store(ctx, metaKey, metaBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH config metadata: %v", err)
	}

	echCfg.privKeyBin = privKeyBytes
	echCfg.configBin = echConfigBytes // this contains the public key
	echCfg.meta = meta

	return echCfg, nil
}

// ECH represents an Encrypted ClientHello configuration.
//
// EXPERIMENTAL: Subject to change.
type ECHConfiguration struct {
	// The public server name that will be used in the outer ClientHello. This
	// should be a domain name for which this server is authoritative, because
	// Caddy will try to provision a certificate for this name. As an outer
	// SNI, it is never used for application data (HTTPS, etc.), but it is
	// necessary for securely reconciling inconsistent client state without
	// breakage and brittleness.
	OuterSNI string `json:"outer_sni,omitempty"`
}

// ECHPublication configures publication of ECH config(s).
type ECHPublication struct {
	// TODO: Should these first two fields be called outer_sni and inner_sni ?

	// The list of ECH configurations to publish, identified by public name.
	// If not set, all configs will be included for publication by default.
	Configs []string `json:"configs,omitempty"`

	// The list of domain names which are protected with the associated ECH
	// configurations ("inner names"). Not all publishers may require this
	// information, but some, like the DNS publisher, do. (The DNS publisher,
	// for example, needs to know for which domain(s) to create DNS records.)
	//
	// If not set, all server names registered with the TLS module will be
	// added to this list implicitly. (Other Caddy apps that use the TLS
	// module automatically register their configured server names for this
	// purpose. For example, the HTTP server registers the hostnames for
	// which it applies automatic HTTPS.)
	//
	// Names in this list should not appear in any other publication config
	// object with the same publishers, since the publications will likely
	// overwrite each other.
	//
	// NOTE: In order to publish ECH configs for domains configured for
	// On-Demand TLS that are not explicitly enumerated elsewhere in the
	// config, those domain names will have to be listed here. The only
	// time Caddy knows which domains it is serving with On-Demand TLS is
	// handshake-time, which is too late for publishing ECH configs; it
	// means the first connections would not protect the server names,
	// revealing that information to observers, and thus defeating the
	// purpose of ECH. Hence the need to list them here so Caddy can
	// proactively publish ECH configs before clients connect with those
	// server names in plaintext.
	Domains []string `json:"domains,omitempty"`

	// How to publish the ECH configurations so clients can know to use them.
	// Note that ECH configs are only published when they are newly created,
	// so adding or changing publishers after the fact will have no effect
	// with existing ECH configs. The next time a config is generated (including
	// when a key is rotated), the current publication modules will be utilized.
	PublishersRaw caddy.ModuleMap `json:"publishers,omitempty" caddy:"namespace=tls.ech.publishers"`
	publishers    []ECHPublisher
}

// ECHDNSProvider can service DNS entries for ECH purposes.
type ECHDNSProvider interface {
	libdns.RecordGetter
	libdns.RecordSetter
}

// ECHDNSPublisher configures how to publish an ECH configuration to
// DNS records for the specified domains.
//
// EXPERIMENTAL: Subject to change.
type ECHDNSPublisher struct {
	// The DNS provider module which will establish the HTTPS record(s).
	ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
	provider    ECHDNSProvider

	logger *zap.Logger
}

// CaddyModule returns the Caddy module information.
func (ECHDNSPublisher) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "tls.ech.publishers.dns",
		New: func() caddy.Module { return new(ECHDNSPublisher) },
	}
}

func (dnsPub ECHDNSPublisher) Provision(ctx caddy.Context) error {
	dnsProvMod, err := ctx.LoadModule(dnsPub, "ProviderRaw")
	if err != nil {
		return fmt.Errorf("loading ECH DNS provider module: %v", err)
	}
	prov, ok := dnsProvMod.(ECHDNSProvider)
	if !ok {
		return fmt.Errorf("ECH DNS provider module is not an ECH DNS Provider: %v", err)
	}
	dnsPub.provider = prov
	dnsPub.logger = ctx.Logger()
	return nil
}

// PublisherKey returns the name of the DNS provider module.
// We intentionally omit specific provider configuration (or a hash thereof,
// since the config is likely sensitive, potentially containing an API key)
// because it is unlikely that specific configuration, such as an API key,
// is relevant to unique key use as an ECH config publisher.
func (dnsPub ECHDNSPublisher) PublisherKey() string {
	return string(dnsPub.provider.(caddy.Module).CaddyModule().ID)
}

// PublishECHConfigList publishes the given ECH config list to the given DNS names.
func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, configListBin []byte) error {
	nameservers := certmagic.RecursiveNameservers(nil) // TODO: we could make resolvers configurable

	for _, domain := range innerNames {
		zone, err := certmagic.FindZoneByFQDN(ctx, dnsPub.logger, domain, nameservers)
		if err != nil {
			dnsPub.logger.Error("could not determine zone for domain",
				zap.String("domain", domain),
				zap.Error(err))
			continue
		}

		// get any existing HTTPS record for this domain, and augment
		// our ech SvcParamKey with any other existing SvcParams
		recs, err := dnsPub.provider.GetRecords(ctx, zone)
		if err != nil {
			dnsPub.logger.Error("unable to get existing DNS records to publish ECH data to HTTPS DNS record",
				zap.String("domain", domain),
				zap.Error(err))
			continue
		}
		relName := libdns.RelativeName(domain+".", zone)
		var httpsRec libdns.Record
		for _, rec := range recs {
			if rec.Name == relName && rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") {
				httpsRec = rec
			}
		}
		params := make(svcParams)
		if httpsRec.Value != "" {
			params, err = parseSvcParams(httpsRec.Value)
			if err != nil {
				dnsPub.logger.Error("unable to parse existing DNS record to publish ECH data to HTTPS DNS record",
					zap.String("domain", domain),
					zap.String("https_rec_value", httpsRec.Value),
					zap.Error(err))
				continue
			}
		}

		// overwrite only the ech SvcParamKey
		params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}

		// publish record
		_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
			{
				// HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460)
				Type:     "HTTPS",
				Name:     relName,
				Priority: 2, // allows a manual override with priority 1
				Target:   ".",
				Value:    params.String(),
				TTL:      1 * time.Minute, // TODO: for testing only
			},
		})
		if err != nil {
			dnsPub.logger.Error("unable to publish ECH data to HTTPS DNS record",
				zap.String("domain", domain),
				zap.Error(err))
			continue
		}
	}

	return nil
}

// echConfig represents an ECHConfig from the specification,
// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
type echConfig struct {
	// "The version of ECH for which this configuration is used.
	// The version is the same as the code point for the
	// encrypted_client_hello extension. Clients MUST ignore any
	// ECHConfig structure with a version they do not support."
	Version uint16

	// The "length" and "contents" fields defined next in the
	// structure are implicitly taken care of by cryptobyte
	// when encoding the following fields:

	// HpkeKeyConfig fields:
	ConfigID     uint8
	KEMID        hpke.KEM
	PublicKey    kem.PublicKey
	CipherSuites []hpkeSymmetricCipherSuite

	// ECHConfigContents fields:
	MaxNameLength uint8
	RawPublicName string
	RawExtensions []byte

	// these fields are not part of the spec, but are here for
	// our use when setting up TLS servers or maintenance
	configBin   []byte
	privKeyBin  []byte
	meta        echConfigMeta
	sendAsRetry bool
}

func (echCfg echConfig) MarshalBinary() ([]byte, error) {
	var b cryptobyte.Builder
	if err := echCfg.marshalBinary(&b); err != nil {
		return nil, err
	}
	return b.Bytes()
}

// UnmarshalBinary decodes the data back into an ECH config.
//
// Borrowed from github.com/OmarTariq612/goech with modifications.
// Original code: Copyright (c) 2023 Omar Tariq AbdEl-Raziq
func (echCfg *echConfig) UnmarshalBinary(data []byte) error {
	var content cryptobyte.String
	b := cryptobyte.String(data)

	if !b.ReadUint16(&echCfg.Version) {
		return errInvalidLen
	}
	if echCfg.Version != draftTLSESNI22 {
		return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI22, echCfg.Version)
	}

	if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() {
		return errInvalidLen
	}

	var t cryptobyte.String
	var pk []byte

	if !content.ReadUint8(&echCfg.ConfigID) ||
		!content.ReadUint16((*uint16)(&echCfg.KEMID)) ||
		!content.ReadUint16LengthPrefixed(&t) ||
		!t.ReadBytes(&pk, len(t)) ||
		!content.ReadUint16LengthPrefixed(&t) ||
		len(t)%4 != 0 /* the length of (KDFs and AEADs) must be divisible by 4 */ {
		return errInvalidLen
	}

	if !echCfg.KEMID.IsValid() {
		return fmt.Errorf("invalid KEM ID: %d", echCfg.KEMID)
	}

	var err error
	if echCfg.PublicKey, err = echCfg.KEMID.Scheme().UnmarshalBinaryPublicKey(pk); err != nil {
		return fmt.Errorf("parsing public_key: %w", err)
	}

	echCfg.CipherSuites = echCfg.CipherSuites[:0]

	for !t.Empty() {
		var hpkeKDF, hpkeAEAD uint16
		if !t.ReadUint16(&hpkeKDF) || !t.ReadUint16(&hpkeAEAD) {
			// we have already checked that the length is divisible by 4
			panic("this must not happen")
		}
		if !hpke.KDF(hpkeKDF).IsValid() {
			return fmt.Errorf("invalid KDF ID: %d", hpkeKDF)
		}
		if !hpke.AEAD(hpkeAEAD).IsValid() {
			return fmt.Errorf("invalid AEAD ID: %d", hpkeAEAD)
		}
		echCfg.CipherSuites = append(echCfg.CipherSuites, hpkeSymmetricCipherSuite{
			KDFID:  hpke.KDF(hpkeKDF),
			AEADID: hpke.AEAD(hpkeAEAD),
		})
	}

	var rawPublicName []byte
	if !content.ReadUint8(&echCfg.MaxNameLength) ||
		!content.ReadUint8LengthPrefixed(&t) ||
		!t.ReadBytes(&rawPublicName, len(t)) ||
		!content.ReadUint16LengthPrefixed(&t) ||
		!t.ReadBytes(&echCfg.RawExtensions, len(t)) ||
		!content.Empty() {
		return errInvalidLen
	}
	echCfg.RawPublicName = string(rawPublicName)

	return nil
}

var errInvalidLen = errors.New("invalid length")

// marshalBinary writes this config to the cryptobyte builder. If there is an error,
// it will occur before any writes have happened.
func (echCfg echConfig) marshalBinary(b *cryptobyte.Builder) error {
	pk, err := echCfg.PublicKey.MarshalBinary()
	if err != nil {
		return err
	}
	if l := len(echCfg.RawPublicName); l == 0 || l > 255 {
		return fmt.Errorf("public name length (%d) must be in the range 1-255", l)
	}

	b.AddUint16(echCfg.Version)
	b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { // "length" field
		b.AddUint8(echCfg.ConfigID)
		b.AddUint16(uint16(echCfg.KEMID))
		b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
			b.AddBytes(pk)
		})
		b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
			for _, cs := range echCfg.CipherSuites {
				b.AddUint16(uint16(cs.KDFID))
				b.AddUint16(uint16(cs.AEADID))
			}
		})
		b.AddUint8(uint8(min(len(echCfg.RawPublicName)+16, 255)))
		b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
			b.AddBytes([]byte(echCfg.RawPublicName))
		})
		b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
			child.AddBytes(echCfg.RawExtensions)
		})
	})

	return nil
}

type hpkeSymmetricCipherSuite struct {
	KDFID  hpke.KDF
	AEADID hpke.AEAD
}

type echConfigList []echConfig

func (cl echConfigList) MarshalBinary() ([]byte, error) {
	var b cryptobyte.Builder
	var err error

	// the list's length prefixes the list, as with most opaque values
	b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
		for _, cfg := range cl {
			if err = cfg.marshalBinary(b); err != nil {
				break
			}
		}
	})
	if err != nil {
		return nil, err
	}

	return b.Bytes()
}

func newECHConfigID(ctx caddy.Context) (uint8, error) {
	// uint8 can be 0-255 inclusive
	const uint8Range = 256

	// avoid repeating storage checks
	tried := make([]bool, uint8Range)

	// Try to find an available number with random rejection sampling;
	// i.e. choose a random number and see if it's already taken.
	// The hard limit on how many times we try to find an available
	// number is flexible... in theory, assuming uniform distribution,
	// 256 attempts should make each possible value show up exactly
	// once, but obviously that won't be the case. We can try more
	// times to try to ensure that every number gets a chance, which
	// is especially useful if few are available, or we can lower it
	// if we assume we should have found an available value by then
	// and want to limit runtime; for now I choose the middle ground
	// and just try as many times as there are possible values.
	for i := 0; i < uint8Range && ctx.Err() == nil; i++ {
		num := uint8(weakrand.N(uint8Range)) //nolint:gosec

		// don't try the same number a second time
		if tried[num] {
			continue
		}
		tried[num] = true

		// check to see if any of the subkeys use this config ID
		numStr := strconv.Itoa(int(num))
		trialPath := path.Join(echConfigsKey, numStr)
		if ctx.Storage().Exists(ctx, trialPath) {
			continue
		}

		return num, nil
	}

	if err := ctx.Err(); err != nil {
		return 0, err
	}

	return 0, fmt.Errorf("depleted attempts to find an available config_id")
}

// svcParams represents SvcParamKey and SvcParamValue pairs as
// described in https://www.rfc-editor.org/rfc/rfc9460 (section 2.1).
type svcParams map[string][]string

// parseSvcParams parses service parameters into a structured type
// for safer manipulation.
func parseSvcParams(input string) (svcParams, error) {
	if len(input) > 4096 {
		return nil, fmt.Errorf("input too long: %d", len(input))
	}

	params := make(svcParams)
	input = strings.TrimSpace(input) + " "

	for cursor := 0; cursor < len(input); cursor++ {
		var key, rawVal string

	keyValPair:
		for i := cursor; i < len(input); i++ {
			switch input[i] {
			case '=':
				key = strings.ToLower(strings.TrimSpace(input[cursor:i]))
				i++
				cursor = i

				var quoted bool
				if input[cursor] == '"' {
					quoted = true
					i++
					cursor = i
				}

				var escaped bool

				for j := cursor; j < len(input); j++ {
					switch input[j] {
					case '"':
						if !quoted {
							return nil, fmt.Errorf("illegal DQUOTE at position %d", j)
						}
						if !escaped {
							// end of quoted value
							rawVal = input[cursor:j]
							j++
							cursor = j
							break keyValPair
						}
					case '\\':
						escaped = true
					case ' ', '\t', '\n', '\r':
						if !quoted {
							// end of unquoted value
							rawVal = input[cursor:j]
							cursor = j
							break keyValPair
						}
					default:
						escaped = false
					}
				}

			case ' ', '\t', '\n', '\r':
				// key with no value (flag)
				key = input[cursor:i]
				params[key] = []string{}
				cursor = i
				break keyValPair
			}
		}

		if rawVal == "" {
			continue
		}

		var sb strings.Builder

		var escape int // start of escape sequence (after \, so 0 is never a valid start)
		for i := 0; i < len(rawVal); i++ {
			ch := rawVal[i]
			if escape > 0 {
				// validate escape sequence
				// (RFC 9460 Appendix A)
				// escaped:   "\" ( non-digit / dec-octet )
				// non-digit: "%x21-2F / %x3A-7E"
				// dec-octet: "0-255 as a 3-digit decimal number"
				if ch >= '0' && ch <= '9' {
					// advance to end of decimal octet, which must be 3 digits
					i += 2
					if i > len(rawVal) {
						return nil, fmt.Errorf("value ends with incomplete escape sequence: %s", rawVal[escape:])
					}
					decOctet, err := strconv.Atoi(rawVal[escape : i+1])
					if err != nil {
						return nil, err
					}
					if decOctet < 0 || decOctet > 255 {
						return nil, fmt.Errorf("invalid decimal octet in escape sequence: %s (%d)", rawVal[escape:i], decOctet)
					}
					sb.WriteRune(rune(decOctet))
					escape = 0
					continue
				} else if (ch < 0x21 || ch > 0x2F) && (ch < 0x3A && ch > 0x7E) {
					return nil, fmt.Errorf("illegal escape sequence %s", rawVal[escape:i])
				}
			}
			switch ch {
			case ';', '(', ')':
				// RFC 9460 Appendix A:
				// > contiguous  = 1*( non-special / escaped )
				// > non-special is VCHAR minus DQUOTE, ";", "(", ")", and "\".
				return nil, fmt.Errorf("illegal character in value %q at position %d: %s", rawVal, i, string(ch))
			case '\\':
				escape = i + 1
			default:
				sb.WriteByte(ch)
				escape = 0
			}
		}

		params[key] = strings.Split(sb.String(), ",")
	}

	return params, nil
}

// String serializes svcParams into zone presentation format.
func (params svcParams) String() string {
	var sb strings.Builder
	for key, vals := range params {
		if sb.Len() > 0 {
			sb.WriteRune(' ')
		}
		sb.WriteString(key)
		var hasVal, needsQuotes bool
		for _, val := range vals {
			if len(val) > 0 {
				hasVal = true
			}
			if strings.ContainsAny(val, `" `) {
				needsQuotes = true
			}
			if hasVal && needsQuotes {
				break
			}
		}
		if hasVal {
			sb.WriteRune('=')
		}
		if needsQuotes {
			sb.WriteRune('"')
		}
		for i, val := range vals {
			if i > 0 {
				sb.WriteRune(',')
			}
			val = strings.ReplaceAll(val, `"`, `\"`)
			val = strings.ReplaceAll(val, `,`, `\,`)
			sb.WriteString(val)
		}
		if needsQuotes {
			sb.WriteRune('"')
		}
	}
	return sb.String()
}

// ECHPublisher is an interface for publishing ECHConfigList values
// so that they can be used by clients.
type ECHPublisher interface {
	// Returns a key that is unique to this publisher and its configuration.
	// A publisher's ID combined with its config is a valid key.
	// It is used to prevent duplicating publications.
	PublisherKey() string

	// Publishes the ECH config list for the given innerNames. Some publishers
	// may not need a list of inner/protected names, and can ignore the argument;
	// most, however, will want to use it to know which inner names are to be
	// associated with the given ECH config list.
	PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error
}

type echConfigMeta struct {
	Created      time.Time          `json:"created"`
	Publications publicationHistory `json:"publications"`
}

// publicationHistory is a map of publisher key to
// map of inner name to timestamp
type publicationHistory map[string]map[string]time.Time

// The key prefix when putting ECH configs in storage. After this
// comes the config ID.
const echConfigsKey = "ech/configs"

// https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html
const draftTLSESNI22 = 0xfe0d

// Interface guard
var _ ECHPublisher = (*ECHDNSPublisher)(nil)