// Copyright 2015 Matthew Holt and The Caddy Authors
//
// 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 httpcaddyfile

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"
	"slices"
	"sort"
	"strconv"
	"strings"

	"github.com/caddyserver/certmagic"
	"github.com/mholt/acmez/v3/acme"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
	"github.com/caddyserver/caddy/v2/modules/caddytls"
)

func (st ServerType) buildTLSApp(
	pairings []sbAddrAssociation,
	options map[string]any,
	warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) {
	tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
	var certLoaders []caddytls.CertificateLoader

	httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
	if hp, ok := options["http_port"].(int); ok {
		httpPort = strconv.Itoa(hp)
	}
	autoHTTPS := []string{}
	if ah, ok := options["auto_https"].([]string); ok {
		autoHTTPS = ah
	}

	// find all hosts that share a server block with a hostless
	// key, so that they don't get forgotten/omitted by auto-HTTPS
	// (since they won't appear in route matchers)
	httpsHostsSharedWithHostlessKey := make(map[string]struct{})
	if !slices.Contains(autoHTTPS, "off") {
		for _, pair := range pairings {
			for _, sb := range pair.serverBlocks {
				for _, addr := range sb.parsedKeys {
					if addr.Host != "" {
						continue
					}

					// this server block has a hostless key, now
					// go through and add all the hosts to the set
					for _, otherAddr := range sb.parsedKeys {
						if otherAddr.Original == addr.Original {
							continue
						}
						if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
							httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
						}
					}
					break
				}
			}
		}
	}

	// a catch-all automation policy is used as a "default" for all subjects that
	// don't have custom configuration explicitly associated with them; this
	// is only to add if the global settings or defaults are non-empty
	catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
	if err != nil {
		return nil, warnings, err
	}
	if catchAllAP != nil {
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
	}

	forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard

	for _, p := range pairings {
		// avoid setting up TLS automation policies for a server that is HTTP-only
		var addresses []string
		for _, addressWithProtocols := range p.addressesWithProtocols {
			addresses = append(addresses, addressWithProtocols.address)
		}
		if !listenersUseAnyPortOtherThan(addresses, httpPort) {
			continue
		}

		for _, sblock := range p.serverBlocks {
			// check the scheme of all the site addresses,
			// skip building AP if they all had http://
			if sblock.isAllHTTP() {
				continue
			}

			// get values that populate an automation policy for this block
			ap, err := newBaseAutomationPolicy(options, warnings, true)
			if err != nil {
				return nil, warnings, err
			}

			sblockHosts := sblock.hostsFromKeys(false)
			if len(sblockHosts) == 0 && catchAllAP != nil {
				ap = catchAllAP
			}

			// on-demand tls
			if _, ok := sblock.pile["tls.on_demand"]; ok {
				ap.OnDemand = true
			}

			// collect hosts that are forced to have certs automated for their specific name
			if _, ok := sblock.pile["tls.force_automate"]; ok {
				for _, host := range sblockHosts {
					forcedAutomatedNames[host] = struct{}{}
				}
			}

			// reuse private keys tls
			if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
				ap.ReusePrivateKeys = true
			}

			if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
				ap.KeyType = keyTypeVals[0].Value.(string)
			}

			if renewalWindowRatioVals, ok := sblock.pile["tls.renewal_window_ratio"]; ok {
				ap.RenewalWindowRatio = renewalWindowRatioVals[0].Value.(float64)
			} else if globalRenewalWindowRatio, ok := options["renewal_window_ratio"]; ok {
				ap.RenewalWindowRatio = globalRenewalWindowRatio.(float64)
			}

			// certificate issuers
			if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
				var issuers []certmagic.Issuer
				for _, issuerVal := range issuerVals {
					issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
				}
				if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
					// this more correctly implements an error check that was removed
					// below; try it with this config:
					//
					// :443 {
					// 	bind 127.0.0.1
					// }
					//
					// :443 {
					// 	bind ::1
					// 	tls {
					// 		issuer acme
					// 	}
					// }
					return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
				}
				ap.Issuers = issuers
			}

			// certificate managers
			if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
				for _, certManager := range certManagerVals {
					certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
					ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
				}
			}
			// custom bind host
			for _, cfgVal := range sblock.pile["bind"] {
				for _, iss := range ap.Issuers {
					// if an issuer was already configured and it is NOT an ACME issuer,
					// skip, since we intend to adjust only ACME issuers; ensure we
					// include any issuer that embeds/wraps an underlying ACME issuer
					var acmeIssuer *caddytls.ACMEIssuer
					if acmeWrapper, ok := iss.(acmeCapable); ok {
						acmeIssuer = acmeWrapper.GetACMEIssuer()
					}
					if acmeIssuer == nil {
						continue
					}

					// proceed to configure the ACME issuer's bind host, without
					// overwriting any existing settings
					if acmeIssuer.Challenges == nil {
						acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
					}
					if acmeIssuer.Challenges.BindHost == "" {
						// only binding to one host is supported
						var bindHost string
						if asserted, ok := cfgVal.Value.(addressesWithProtocols); ok && len(asserted.addresses) > 0 {
							bindHost = asserted.addresses[0]
						}
						acmeIssuer.Challenges.BindHost = bindHost
					}
				}
			}

			// we used to ensure this block is allowed to create an automation policy;
			// doing so was forbidden if it has a key with no host (i.e. ":443")
			// and if there is a different server block that also has a key with no
			// host -- since a key with no host matches any host, we need its
			// associated automation policy to have an empty Subjects list, i.e. no
			// host filter, which is indistinguishable between the two server blocks
			// because automation is not done in the context of a particular server...
			// this is an example of a poor mapping from Caddyfile to JSON but that's
			// the least-leaky abstraction I could figure out -- however, this check
			// was preventing certain listeners, like those provided by plugins, from
			// being used as desired (see the Tailscale listener plugin), so I removed
			// the check: and I think since I originally wrote the check I added a new
			// check above which *properly* detects this ambiguity without breaking the
			// listener plugin; see the check above with a commented example config
			if len(sblockHosts) == 0 && catchAllAP == nil {
				// this server block has a key with no hosts, but there is not yet
				// a catch-all automation policy (probably because no global options
				// were set), so this one becomes it
				catchAllAP = ap
			}

			hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
			sort.Strings(hostsNotHTTP) // solely for deterministic test results

			// associate our new automation policy with this server block's hosts
			ap.SubjectsRaw = hostsNotHTTP

			// if a combination of public and internal names were given
			// for this same server block and no issuer was specified, we
			// need to separate them out in the automation policies so
			// that the internal names can use the internal issuer and
			// the other names can use the default/public/ACME issuer
			var ap2 *caddytls.AutomationPolicy
			if len(ap.Issuers) == 0 {
				var internal, external []string
				for _, s := range ap.SubjectsRaw {
					// do not create Issuers for Tailscale domains; they will be given a Manager instead
					if isTailscaleDomain(s) {
						continue
					}
					if !certmagic.SubjectQualifiesForCert(s) {
						return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
					}
					// we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
					// names like *.*.tld that may not qualify for a public certificate are actually
					// fine when used with OnDemand, since OnDemand (currently) does not obtain
					// wildcards (if it ever does, there will be a separate config option to enable
					// it that we would need to check here) since the hostname is known at handshake;
					// and it is unexpected to switch to internal issuer when the user wants to get
					// regular certificates on-demand for a class of certs like *.*.tld.
					if subjectQualifiesForPublicCert(ap, s) {
						external = append(external, s)
					} else {
						internal = append(internal, s)
					}
				}
				if len(external) > 0 && len(internal) > 0 {
					ap.SubjectsRaw = external
					apCopy := *ap
					ap2 = &apCopy
					ap2.SubjectsRaw = internal
					ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
				}
			}

			if tlsApp.Automation == nil {
				tlsApp.Automation = new(caddytls.AutomationConfig)
			}
			tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
			if ap2 != nil {
				tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
			}

			// certificate loaders
			if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
				for _, clVal := range clVals {
					certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
				}
			}
		}
	}

	// group certificate loaders by module name, then add to config
	if len(certLoaders) > 0 {
		loadersByName := make(map[string]caddytls.CertificateLoader)
		for _, cl := range certLoaders {
			name := caddy.GetModuleName(cl)
			// ugh... technically, we may have multiple FileLoader and FolderLoader
			// modules (because the tls directive returns one per occurrence), but
			// the config structure expects only one instance of each kind of loader
			// module, so we have to combine them... instead of enumerating each
			// possible cert loader module in a type switch, we can use reflection,
			// which works on any cert loaders that are slice types
			if reflect.TypeOf(cl).Kind() == reflect.Slice {
				combined := reflect.ValueOf(loadersByName[name])
				if !combined.IsValid() {
					combined = reflect.New(reflect.TypeOf(cl)).Elem()
				}
				clVal := reflect.ValueOf(cl)
				for i := range clVal.Len() {
					combined = reflect.Append(combined, clVal.Index(i))
				}
				loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
			}
		}
		for certLoaderName, loaders := range loadersByName {
			tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
		}
	}

	// set any of the on-demand options, for if/when on-demand TLS is enabled
	if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok {
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.OnDemand = onDemand
	}

	// set up "global" (to the TLS app) DNS provider config
	if globalDNS, ok := options["dns"]; ok && globalDNS != nil {
		tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
	}

	// set up "global" (to the TLS app) DNS resolvers config
	if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil {
		tlsApp.Resolvers = globalResolvers.([]string)
	}

	// set up ECH from Caddyfile options
	if ech, ok := options["ech"].(*caddytls.ECH); ok {
		tlsApp.EncryptedClientHello = ech

		// outer server names will need certificates, so make sure they're included
		// in an automation policy for them that applies any global options
		ap, err := newBaseAutomationPolicy(options, warnings, true)
		if err != nil {
			return nil, warnings, err
		}
		for _, cfg := range ech.Configs {
			if cfg.PublicName != "" {
				ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
			}
		}
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
	}

	// if the storage clean interval is a boolean, then it's "off" to disable cleaning
	if sc, ok := options["storage_check"].(string); ok && sc == "off" {
		tlsApp.DisableStorageCheck = true
	}

	// if the storage clean interval is a boolean, then it's "off" to disable cleaning
	if sci, ok := options["storage_clean_interval"].(bool); ok && !sci {
		tlsApp.DisableStorageClean = true
	}

	// set the storage clean interval if configured
	if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.StorageCleanInterval = storageCleanInterval
	}

	// set the expired certificates renew interval if configured
	if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.RenewCheckInterval = renewCheckInterval
	}

	// set the OCSP check interval if configured
	if ocspCheckInterval, ok := options["ocsp_interval"].(caddy.Duration); ok {
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.OCSPCheckInterval = ocspCheckInterval
	}

	// set whether OCSP stapling should be disabled for manually-managed certificates
	if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
		tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
	}

	// if any hostnames appear on the same server block as a key with
	// no host, they will not be used with route matchers because the
	// hostless key matches all hosts, therefore, it wouldn't be
	// considered for auto-HTTPS, so we need to make sure those hosts
	// are manually considered for managed certificates; we also need
	// to make sure that any of these names which are internal-only
	// get internal certificates by default rather than ACME
	var al caddytls.AutomateLoader
	internalAP := &caddytls.AutomationPolicy{
		IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
	}
	if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
		for h := range httpsHostsSharedWithHostlessKey {
			al = append(al, h)
			if !certmagic.SubjectQualifiesForPublicCert(h) {
				internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, h)
			}
		}
	}
	for name := range forcedAutomatedNames {
		if slices.Contains(al, name) {
			continue
		}
		al = append(al, name)
	}
	slices.Sort(al) // to stabilize the adapt output
	if len(al) > 0 {
		tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
	}
	if len(internalAP.SubjectsRaw) > 0 {
		if tlsApp.Automation == nil {
			tlsApp.Automation = new(caddytls.AutomationConfig)
		}
		tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
	}

	// if there are any global options set for issuers (ACME ones in particular), make sure they
	// take effect in every automation policy that does not have any issuers
	if tlsApp.Automation != nil {
		globalEmail := options["email"]
		globalACMECA := options["acme_ca"]
		globalACMECARoot := options["acme_ca_root"]
		_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
		globalACMEEAB := options["acme_eab"]
		globalPreferredChains := options["preferred_chains"]
		hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil
		if hasGlobalACMEDefaults {
			for i := range tlsApp.Automation.Policies {
				ap := tlsApp.Automation.Policies[i]
				if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
					// for public names, create default issuers which will later be filled in with configured global defaults
					// (internal names will implicitly use the internal issuer at auto-https time)
					emailStr, _ := globalEmail.(string)
					ap.Issuers = caddytls.DefaultIssuers(emailStr)

					// if a specific endpoint is configured, can't use multiple default issuers
					if globalACMECA != nil {
						ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
					}
				}
			}
		}
	}

	// finalize and verify policies; do cleanup
	if tlsApp.Automation != nil {
		for i, ap := range tlsApp.Automation.Policies {
			// ensure all issuers have global defaults filled in
			for j, issuer := range ap.Issuers {
				err := fillInGlobalACMEDefaults(issuer, options)
				if err != nil {
					return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
				}
			}

			// encode all issuer values we created, so they will be rendered in the output
			if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
				for _, iss := range ap.Issuers {
					issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
					ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
				}
			}
		}

		// consolidate automation policies that are the exact same
		tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)

		// ensure automation policies don't overlap subjects (this should be
		// an error at provision-time as well, but catch it in the adapt phase
		// for convenience)
		automationHostSet := make(map[string]struct{})
		for _, ap := range tlsApp.Automation.Policies {
			for _, s := range ap.SubjectsRaw {
				if _, ok := automationHostSet[s]; ok {
					return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
				}
				automationHostSet[s] = struct{}{}
			}
		}

		// if nothing remains, remove any excess values to clean up the resulting config
		if len(tlsApp.Automation.Policies) == 0 {
			tlsApp.Automation.Policies = nil
		}
		if reflect.DeepEqual(tlsApp.Automation, new(caddytls.AutomationConfig)) {
			tlsApp.Automation = nil
		}
	}

	return tlsApp, warnings, nil
}

type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }

func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) error {
	acmeWrapper, ok := issuer.(acmeCapable)
	if !ok {
		return nil
	}
	acmeIssuer := acmeWrapper.GetACMEIssuer()
	if acmeIssuer == nil {
		return nil
	}

	globalEmail := options["email"]
	globalACMECA := options["acme_ca"]
	globalACMECARoot := options["acme_ca_root"]
	globalACMEDNS, globalACMEDNSok := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
	globalACMEEAB := options["acme_eab"]
	globalPreferredChains := options["preferred_chains"]
	globalCertLifetime := options["cert_lifetime"]
	globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
	globalDefaultBind := options["default_bind"]

	if globalEmail != nil && acmeIssuer.Email == "" {
		acmeIssuer.Email = globalEmail.(string)
	}
	if globalACMECA != nil && acmeIssuer.CA == "" {
		acmeIssuer.CA = globalACMECA.(string)
	}
	if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
		acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
	}
	if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil || acmeIssuer.Challenges.DNS.ProviderRaw == nil) {
		globalDNS := options["dns"]
		if globalDNS == nil && globalACMEDNS == nil {
			return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
		}
		if acmeIssuer.Challenges == nil {
			acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
		}
		if acmeIssuer.Challenges.DNS == nil {
			acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
		}
		if globalACMEDNS != nil && acmeIssuer.Challenges.DNS.ProviderRaw == nil {
			// Set a global DNS provider if `acme_dns` is set
			acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
		}
	}
	if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
		acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
	}
	if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
		acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
	}
	// only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others)
	if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
		if acmeIssuer.Challenges == nil {
			acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
		}
		if acmeIssuer.Challenges.HTTP == nil {
			acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
		}
		acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
	}
	if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
		if acmeIssuer.Challenges == nil {
			acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
		}
		if acmeIssuer.Challenges.TLSALPN == nil {
			acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
		}
		acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
	}
	// If BindHost is still unset, fall back to the first default_bind address if set
	// This avoids binding the automation policy to the wildcard socket, which is unexpected behavior when a more selective socket is specified via default_bind
	// In BSD it is valid to bind to the wildcard socket even though a more selective socket is already open (still unexpected behavior by the caller though)
	// In Linux the same call will error with EADDRINUSE whenever the listener for the automation policy is opened
	if acmeIssuer.Challenges == nil || (acmeIssuer.Challenges.DNS == nil && acmeIssuer.Challenges.BindHost == "") {
		if defBinds, ok := globalDefaultBind.([]ConfigValue); ok && len(defBinds) > 0 {
			if abp, ok := defBinds[0].Value.(addressesWithProtocols); ok && len(abp.addresses) > 0 {
				if acmeIssuer.Challenges == nil {
					acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
				}
				acmeIssuer.Challenges.BindHost = abp.addresses[0]
			}
		}
	}
	if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
		acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
	}
	// apply global resolvers if DNS challenge is configured and resolvers are not already set
	globalResolvers := options["tls_resolvers"]
	if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
		// Check if DNS challenge is actually configured
		hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil
		if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 {
			acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string)
		}
	}
	return nil
}

// implicitACMEIssuers returns the issuers to use for ACME-related tls
// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options
// configure ACME issuers, those become the templates for the local shortcut
// configuration; otherwise, default ACME issuers are used.
func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer {
	globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer)

	var implicitIssuers []certmagic.Issuer
	for _, issuer := range globalIssuers {
		acmeWrapper, ok := issuer.(acmeCapable)
		if !ok {
			continue
		}
		baseIssuer := acmeWrapper.GetACMEIssuer()
		if baseIssuer == nil {
			continue
		}
		implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
	}
	if len(implicitIssuers) > 0 {
		return implicitIssuers
	}

	// If an ACME CA endpoint was set locally, the user expects to use only that
	// CA rather than the usual default fallback issuers.
	defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
	if acmeIssuer.CA != "" {
		defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
	}

	implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers))
	for _, issuer := range defaultIssuers {
		acmeWrapper, ok := issuer.(acmeCapable)
		if !ok {
			implicitIssuers = append(implicitIssuers, issuer)
			continue
		}
		baseIssuer := acmeWrapper.GetACMEIssuer()
		if baseIssuer == nil {
			implicitIssuers = append(implicitIssuers, issuer)
			continue
		}
		implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
	}
	return implicitIssuers
}

func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
	if base == nil {
		return cloneACMEIssuer(overrides)
	}

	merged := cloneACMEIssuer(base)
	if overrides == nil {
		return merged
	}

	if overrides.CA != "" {
		merged.CA = overrides.CA
	}
	if overrides.TestCA != "" {
		merged.TestCA = overrides.TestCA
	}
	if overrides.Email != "" {
		merged.Email = overrides.Email
	}
	if overrides.Profile != "" {
		merged.Profile = overrides.Profile
	}
	if overrides.AccountKey != "" {
		merged.AccountKey = overrides.AccountKey
	}
	if overrides.ExternalAccount != nil {
		merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount)
	}
	if overrides.ACMETimeout != 0 {
		merged.ACMETimeout = overrides.ACMETimeout
	}
	if len(overrides.TrustedRootsPEMFiles) > 0 {
		merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...)
	}
	if overrides.PreferredChains != nil {
		merged.PreferredChains = cloneChainPreference(overrides.PreferredChains)
	}
	if overrides.CertificateLifetime != 0 {
		merged.CertificateLifetime = overrides.CertificateLifetime
	}
	if len(overrides.NetworkProxyRaw) > 0 {
		merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw)
	}
	merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges)

	return merged
}

func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
	if base == nil {
		return cloneChallengesConfig(overrides)
	}
	merged := cloneChallengesConfig(base)
	if overrides == nil {
		return merged
	}

	merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP)
	merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN)
	merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS)
	if overrides.BindHost != "" {
		merged.BindHost = overrides.BindHost
	}
	if overrides.Distributed != nil {
		value := *overrides.Distributed
		merged.Distributed = &value
	}

	return merged
}

func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
	if base == nil {
		return cloneHTTPChallengeConfig(overrides)
	}
	merged := cloneHTTPChallengeConfig(base)
	if overrides == nil {
		return merged
	}

	if overrides.Disabled {
		merged.Disabled = true
	}
	if overrides.AlternatePort != 0 {
		merged.AlternatePort = overrides.AlternatePort
	}

	return merged
}

func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
	if base == nil {
		return cloneTLSALPNChallengeConfig(overrides)
	}
	merged := cloneTLSALPNChallengeConfig(base)
	if overrides == nil {
		return merged
	}

	if overrides.Disabled {
		merged.Disabled = true
	}
	if overrides.AlternatePort != 0 {
		merged.AlternatePort = overrides.AlternatePort
	}

	return merged
}

func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
	if base == nil {
		return cloneDNSChallengeConfig(overrides)
	}
	merged := cloneDNSChallengeConfig(base)
	if overrides == nil {
		return merged
	}

	if len(overrides.ProviderRaw) > 0 {
		merged.ProviderRaw = slices.Clone(overrides.ProviderRaw)
	}
	if overrides.PropagationDelay != 0 {
		merged.PropagationDelay = overrides.PropagationDelay
	}
	if overrides.PropagationTimeout != 0 {
		merged.PropagationTimeout = overrides.PropagationTimeout
	}
	if overrides.Resolvers != nil {
		merged.Resolvers = slices.Clone(overrides.Resolvers)
	}
	if overrides.OverrideDomain != "" {
		merged.OverrideDomain = overrides.OverrideDomain
	}
	if overrides.TTL != 0 {
		merged.TTL = overrides.TTL
	}

	return merged
}

func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
	if iss == nil {
		return nil
	}

	cloned := *iss
	cloned.Challenges = cloneChallengesConfig(iss.Challenges)
	cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount)
	cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles)
	cloned.PreferredChains = cloneChainPreference(iss.PreferredChains)
	cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw)

	return &cloned
}

func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
	if cfg == nil {
		return nil
	}

	cloned := *cfg
	cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP)
	cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN)
	cloned.DNS = cloneDNSChallengeConfig(cfg.DNS)
	if cfg.Distributed != nil {
		value := *cfg.Distributed
		cloned.Distributed = &value
	}

	return &cloned
}

func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
	if cfg == nil {
		return nil
	}

	cloned := *cfg
	return &cloned
}

func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
	if cfg == nil {
		return nil
	}

	cloned := *cfg
	return &cloned
}

func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
	if cfg == nil {
		return nil
	}

	cloned := *cfg
	cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw)
	cloned.Resolvers = slices.Clone(cfg.Resolvers)

	return &cloned
}

func cloneACMEEAB(eab *acme.EAB) *acme.EAB {
	if eab == nil {
		return nil
	}

	cloned := *eab
	return &cloned
}

func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference {
	if pref == nil {
		return nil
	}

	cloned := *pref
	cloned.RootCommonName = slices.Clone(pref.RootCommonName)
	cloned.AnyCommonName = slices.Clone(pref.AnyCommonName)
	if pref.Smallest != nil {
		value := *pref.Smallest
		cloned.Smallest = &value
	}

	return &cloned
}

func appendUniqueStrings(existing []string, additions ...string) []string {
	for _, value := range additions {
		if !slices.Contains(existing, value) {
			existing = append(existing, value)
		}
	}
	return existing
}

// newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(
	options map[string]any,
	_ []caddyconfig.Warning,
	always bool,
) (*caddytls.AutomationPolicy, error) {
	issuers, hasIssuers := options["cert_issuer"]
	_, hasLocalCerts := options["local_certs"]
	keyType, hasKeyType := options["key_type"]
	ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
	renewalWindowRatio, hasRenewalWindowRatio := options["renewal_window_ratio"]
	hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling || hasRenewalWindowRatio

	globalACMECA := options["acme_ca"]
	globalACMECARoot := options["acme_ca_root"]
	_, globalACMEDNS := options["acme_dns"] // can be set to nil (to use globally-defined "dns" value instead), but it is still set
	globalACMEEAB := options["acme_eab"]
	globalPreferredChains := options["preferred_chains"]
	hasGlobalACMEDefaults := globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS || globalACMEEAB != nil || globalPreferredChains != nil

	// if there are no global options related to automation policies
	// set, then we can just return right away
	if !hasGlobalAutomationOpts && !hasGlobalACMEDefaults {
		if always {
			return new(caddytls.AutomationPolicy), nil
		}
		return nil, nil
	}

	ap := new(caddytls.AutomationPolicy)
	if hasKeyType {
		ap.KeyType = keyType.(string)
	}

	if hasIssuers && hasLocalCerts {
		return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
	}

	if hasIssuers {
		ap.Issuers = issuers.([]certmagic.Issuer)
	} else if hasLocalCerts {
		ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
	}

	if hasGlobalACMEDefaults {
		for i := range ap.Issuers {
			if err := fillInGlobalACMEDefaults(ap.Issuers[i], options); err != nil {
				return nil, fmt.Errorf("filling in global issuer defaults for issuer %d: %v", i, err)
			}
		}
	}

	if hasOCSPStapling {
		ocspConfig := ocspStapling.(certmagic.OCSPConfig)
		ap.DisableOCSPStapling = ocspConfig.DisableStapling
		ap.OCSPOverrides = ocspConfig.ResponderOverrides
	}

	if hasRenewalWindowRatio {
		ap.RenewalWindowRatio = renewalWindowRatio.(float64)
	}

	return ap, nil
}

// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
	// sort from most specific to least specific; we depend on this ordering
	sort.SliceStable(aps, func(i, j int) bool {
		if automationPolicyIsSubset(aps[i], aps[j]) {
			return true
		}
		if automationPolicyIsSubset(aps[j], aps[i]) {
			return false
		}
		return len(aps[i].SubjectsRaw) > len(aps[j].SubjectsRaw)
	})

	emptyAPCount := 0
	origLenAPs := len(aps)
	// compute the number of empty policies (disregarding subjects) - see #4128
	// while we're at it,
	emptyAP := new(caddytls.AutomationPolicy)
	for i := 0; i < len(aps); i++ {
		emptyAP.SubjectsRaw = aps[i].SubjectsRaw
		emptyAP.ManagersRaw = nil
		if reflect.DeepEqual(aps[i], emptyAP) {
			// AP is empty
			emptyAPCount++

			// see if this AP shadows something later
			shadowIdx := automationPolicyShadows(i, aps)
			emptyAP.SubjectsRaw = nil
			if shadowIdx >= 0 {
				emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw
				// allow the later policy, which is likely for a wildcard, to have cert
				// managers ("get_certificate"), since wildcards now cover specific
				// subdomains by default, when configured (see discussion in #7559)
				emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw
			}

			// if this is the last AP, we can delete it, since auto-https should
			// pick it up; if it shadows something later that is also empty, we
			// can similarly delete this; but if it shadows something that is NOT
			// empty, we must not delete it since the shadowing has a purpose
			if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) {
				aps = slices.Delete(aps, i, i+1)
				i--
			}
		}
	}
	// If all policies are empty, we can return nil, as there is no need to set any policy
	if emptyAPCount == origLenAPs {
		return nil
	}

	// remove or combine duplicate policies
outer:
	for i := 0; i < len(aps); i++ {
		// compare only with next policies; we sorted by specificity so we must not delete earlier policies
		for j := i + 1; j < len(aps); j++ {
			// if they're exactly equal in every way, just keep one of them
			if reflect.DeepEqual(aps[i], aps[j]) {
				aps = slices.Delete(aps, j, j+1)
				// must re-evaluate current i against next j; can't skip it!
				// even if i decrements to -1, will be incremented to 0 immediately
				i--
				continue outer
			}

			// if the policy is the same, we can keep just one, but we have
			// to be careful which one we keep; if only one has any hostnames
			// defined, then we need to keep the one without any hostnames,
			// otherwise the one without any subjects (a catch-all) would be
			// eaten up by the one with subjects; and if both have subjects, we
			// need to combine their lists
			if automationPoliciesHaveSameIssuers(aps[i], aps[j]) &&
				reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
				bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
				aps[i].MustStaple == aps[j].MustStaple &&
				aps[i].KeyType == aps[j].KeyType &&
				aps[i].OnDemand == aps[j].OnDemand &&
				aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys &&
				aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
				if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 {
					// later policy (at j) has no subjects ("catch-all"), so we can
					// remove the identical-but-more-specific policy that comes first
					// AS LONG AS it is not shadowed by another policy before it; e.g.
					// if policy i is for example.com, policy i+1 is '*.com', and policy
					// j is catch-all, we cannot remove policy i because that would
					// cause example.com to be served by the less specific policy for
					// '*.com', which might be different (yes we've seen this happen)
					if automationPolicyShadows(i, aps) >= j {
						aps = slices.Delete(aps, i, i+1)
						i--
						continue outer
					}
				} else {
					// avoid repeated subjects
					for _, subj := range aps[j].SubjectsRaw {
						if !slices.Contains(aps[i].SubjectsRaw, subj) {
							aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
						}
					}
					aps = slices.Delete(aps, j, j+1)
					j--
				}
			}
		}
	}

	return aps
}

// automationPolicyIsSubset returns true if a's subjects are a subset
// of b's subjects.
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
	if len(b.SubjectsRaw) == 0 {
		return true
	}
	if len(a.SubjectsRaw) == 0 {
		return false
	}
	for _, aSubj := range a.SubjectsRaw {
		inSuperset := slices.ContainsFunc(b.SubjectsRaw, func(bSubj string) bool {
			return certmagic.MatchWildcard(aSubj, bSubj)
		})
		if !inSuperset {
			return false
		}
	}
	return true
}

// automationPolicyShadows returns the index of a policy that aps[i] shadows;
// in other words, for all policies after position i, if that policy covers
// the same subjects but is less specific, that policy's position is returned,
// or -1 if no shadowing is found. For example, if policy i is for
// "foo.example.com" and policy i+2 is for "*.example.com", then i+2 will be
// returned, since that policy is shadowed by i, which is in front.
func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
	for j := i + 1; j < len(aps); j++ {
		if automationPolicyIsSubset(aps[i], aps[j]) {
			return j
		}
	}
	return -1
}

// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
//
// IP subjects are considered as non-qualifying for public certs. Technically, there are
// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
// is used solely for implicit automation (defaults), where it gets really complicated to
// keep track of which issuers support IP certificates in which circumstances. Currently,
// issuers that support IP certificates are very few, and all require some sort of config
// from the user anyway (such as an account credential). Since we cannot implicitly and
// automatically get public IP certs without configuration from the user, we treat IPs as
// not qualifying for public certificates. Users should expressly configure an issuer
// that supports IP certs for that purpose.
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
	return !certmagic.SubjectIsIP(subj) &&
		!certmagic.SubjectIsInternal(subj) &&
		(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}

func automationPoliciesHaveSameIssuers(a, b *caddytls.AutomationPolicy) bool {
	if reflect.DeepEqual(a.IssuersRaw, b.IssuersRaw) {
		return automationPoliciesHaveCompatibleImplicitIssuers(a, b)
	}
	return automationPolicyUsesDefaultInternalIssuer(a) && automationPolicyUsesDefaultInternalIssuer(b)
}

func automationPolicyUsesDefaultInternalIssuer(ap *caddytls.AutomationPolicy) bool {
	if len(ap.IssuersRaw) == 0 && len(ap.Issuers) == 0 {
		return automationPolicyImplicitIssuerClass(ap) == "internal"
	}
	return len(ap.IssuersRaw) == 1 &&
		len(ap.Issuers) == 0 &&
		string(bytes.TrimSpace(ap.IssuersRaw[0])) == `{"module":"internal"}`
}

// automationPoliciesHaveCompatibleImplicitIssuers returns whether two policies
// without explicit issuers can be consolidated without changing default issuer
// selection for their subjects.
func automationPoliciesHaveCompatibleImplicitIssuers(a, b *caddytls.AutomationPolicy) bool {
	if len(a.IssuersRaw) > 0 || len(a.Issuers) > 0 ||
		len(b.IssuersRaw) > 0 || len(b.Issuers) > 0 {
		return true
	}

	aClass := automationPolicyImplicitIssuerClass(a)
	bClass := automationPolicyImplicitIssuerClass(b)
	return aClass == "catch-all" || bClass == "catch-all" || aClass == bClass
}

func automationPolicyImplicitIssuerClass(ap *caddytls.AutomationPolicy) string {
	if len(ap.SubjectsRaw) == 0 {
		return "catch-all"
	}

	hasPublic := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
		return subjectQualifiesForPublicCert(ap, subj)
	})
	hasInternal := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
		return !subjectQualifiesForPublicCert(ap, subj)
	})

	switch {
	case hasPublic && hasInternal:
		return "mixed"
	case hasPublic:
		return "public"
	default:
		return "internal"
	}
}

// automationPolicyHasAllPublicNames returns true if all the names on the policy
// do NOT qualify for public certs OR are tailscale domains.
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
	return !slices.ContainsFunc(ap.SubjectsRaw, func(i string) bool {
		return !subjectQualifiesForPublicCert(ap, i) || isTailscaleDomain(i)
	})
}

func isTailscaleDomain(name string) bool {
	return strings.HasSuffix(strings.ToLower(name), ".ts.net")
}
