package dnsforward

import (
	"cmp"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/netip"
	"slices"
	"time"

	"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
	"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
	"github.com/AdguardTeam/AdGuardHome/internal/aghslog"
	"github.com/AdguardTeam/AdGuardHome/internal/filtering"
	"github.com/AdguardTeam/dnsproxy/proxy"
	"github.com/AdguardTeam/dnsproxy/upstream"
	"github.com/AdguardTeam/golibs/errors"
	"github.com/AdguardTeam/golibs/logutil/slogutil"
	"github.com/AdguardTeam/golibs/netutil"
	"github.com/AdguardTeam/golibs/stringutil"
	"github.com/AdguardTeam/golibs/validate"
)

// jsonDNSConfig is the JSON representation of the DNS server configuration.
//
// TODO(s.chzhen):  Split it into smaller pieces.  Use aghalg.NullBool instead
// of *bool.
type jsonDNSConfig struct {
	// Upstreams is the list of upstream DNS servers.
	Upstreams *[]string `json:"upstream_dns"`

	// UpstreamsFile is the file containing upstream DNS servers.
	UpstreamsFile *string `json:"upstream_dns_file"`

	// Bootstraps is the list of DNS servers resolving IP addresses of the
	// upstream DoH/DoT resolvers.
	Bootstraps *[]string `json:"bootstrap_dns"`

	// Fallbacks is the list of fallback DNS servers used when upstream DNS
	// servers are not responding.
	Fallbacks *[]string `json:"fallback_dns"`

	// ProtectionEnabled defines if protection is enabled.
	ProtectionEnabled *bool `json:"protection_enabled"`

	// Ratelimit is the number of requests per second allowed per client.
	Ratelimit *uint32 `json:"ratelimit"`

	// RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for
	// rate limiting requests.
	RatelimitSubnetLenIPv4 *uint `json:"ratelimit_subnet_len_ipv4"`

	// RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for
	// rate limiting requests.
	RatelimitSubnetLenIPv6 *uint `json:"ratelimit_subnet_len_ipv6"`

	// UpstreamTimeout is an upstream timeout in seconds.
	UpstreamTimeout *int `json:"upstream_timeout"`

	// RatelimitWhitelist is a list of IP addresses excluded from rate limiting.
	RatelimitWhitelist *[]netip.Addr `json:"ratelimit_whitelist"`

	// BlockingMode defines the way blocked responses are constructed.
	BlockingMode *filtering.BlockingMode `json:"blocking_mode"`

	// EDNSCSEnabled defines if EDNS Client Subnet is enabled.
	EDNSCSEnabled *bool `json:"edns_cs_enabled"`

	// EDNSCSUseCustom defines if EDNSCSCustomIP should be used.
	EDNSCSUseCustom *bool `json:"edns_cs_use_custom"`

	// DNSSECEnabled defines if DNSSEC is enabled.
	DNSSECEnabled *bool `json:"dnssec_enabled"`

	// DisableIPv6 defines if IPv6 addresses should be dropped.
	DisableIPv6 *bool `json:"disable_ipv6"`

	// UpstreamMode defines the way DNS requests are constructed.
	UpstreamMode *jsonUpstreamMode `json:"upstream_mode"`

	// BlockedResponseTTL is the TTL for blocked responses.
	BlockedResponseTTL *uint32 `json:"blocked_response_ttl"`

	// CacheSize in bytes.
	CacheSize *uint32 `json:"cache_size"`

	// CacheMinTTL is custom minimum TTL for cached DNS responses.
	CacheMinTTL *uint32 `json:"cache_ttl_min"`

	// CacheMaxTTL is custom maximum TTL for cached DNS responses.
	CacheMaxTTL *uint32 `json:"cache_ttl_max"`

	// CacheEnabled defines if the DNS cache should be used.
	CacheEnabled *bool `json:"cache_enabled"`

	// CacheOptimistic defines if expired entries should be served.
	CacheOptimistic *bool `json:"cache_optimistic"`

	// ResolveClients defines if clients IPs should be resolved into hostnames.
	ResolveClients *bool `json:"resolve_clients"`

	// UsePrivateRDNS defines if privates DNS resolvers should be used.
	UsePrivateRDNS *bool `json:"use_private_ptr_resolvers"`

	// LocalPTRUpstreams is the list of local private DNS resolvers.
	LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`

	// BlockingIPv4 is custom IPv4 address for blocked A requests.
	BlockingIPv4 netip.Addr `json:"blocking_ipv4"`

	// BlockingIPv6 is custom IPv6 address for blocked AAAA requests.
	BlockingIPv6 netip.Addr `json:"blocking_ipv6"`

	// DisabledUntil is a timestamp until when the protection is disabled.
	DisabledUntil *time.Time `json:"protection_disabled_until"`

	// EDNSCSCustomIP is custom IP for EDNS Client Subnet.
	EDNSCSCustomIP netip.Addr `json:"edns_cs_custom_ip"`

	// DefaultLocalPTRUpstreams is used to pass the addresses from
	// systemResolvers to the front-end.  It's not a pointer to the slice since
	// there is no need to omit it while decoding from JSON.
	DefaultLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"`
}

// jsonUpstreamMode is a enumeration of upstream modes.
type jsonUpstreamMode string

const (
	// jsonUpstreamModeEmpty is the default value on frontend, it is used as
	// jsonUpstreamModeLoadBalance mode.
	//
	// Deprecated: Use jsonUpstreamModeLoadBalance instead.
	jsonUpstreamModeEmpty jsonUpstreamMode = ""

	jsonUpstreamModeLoadBalance jsonUpstreamMode = "load_balance"
	jsonUpstreamModeParallel    jsonUpstreamMode = "parallel"
	jsonUpstreamModeFastestAddr jsonUpstreamMode = "fastest_addr"
)

func (s *Server) getDNSConfig(ctx context.Context) (c *jsonDNSConfig) {
	protectionEnabled, protectionDisabledUntil := s.UpdatedProtectionStatus(ctx)

	s.serverLock.RLock()
	defer s.serverLock.RUnlock()

	upstreams := stringutil.CloneSliceOrEmpty(s.conf.UpstreamDNS)
	upstreamFile := s.conf.UpstreamDNSFileName
	bootstraps := stringutil.CloneSliceOrEmpty(s.conf.BootstrapDNS)
	fallbacks := stringutil.CloneSliceOrEmpty(s.conf.FallbackDNS)
	blockingMode, blockingIPv4, blockingIPv6 := s.dnsFilter.BlockingMode()
	blockedResponseTTL := s.dnsFilter.BlockedResponseTTL()
	ratelimit := s.conf.Ratelimit
	ratelimitSubnetLenIPv4 := s.conf.RatelimitSubnetLenIPv4
	ratelimitSubnetLenIPv6 := s.conf.RatelimitSubnetLenIPv6
	ratelimitWhitelist := append([]netip.Addr{}, s.conf.RatelimitWhitelist...)
	upstreamTimeout := int(s.conf.UpstreamTimeout.Seconds())

	customIP := s.conf.EDNSClientSubnet.CustomIP
	enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
	useCustom := s.conf.EDNSClientSubnet.UseCustom

	enableDNSSEC := s.conf.EnableDNSSEC
	aaaaDisabled := s.conf.AAAADisabled
	cacheEnabled := s.conf.CacheEnabled
	cacheSize := s.conf.CacheSize
	cacheMinTTL := s.conf.CacheMinTTL
	cacheMaxTTL := s.conf.CacheMaxTTL
	cacheOptimistic := s.conf.CacheOptimistic
	resolveClients := s.conf.AddrProcConf.UseRDNS
	usePrivateRDNS := s.conf.UsePrivateRDNS
	localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)

	var upstreamMode jsonUpstreamMode
	switch s.conf.UpstreamMode {
	case UpstreamModeLoadBalance:
		// TODO(d.kolyshev): Support jsonUpstreamModeLoadBalance on frontend instead
		// of jsonUpstreamModeEmpty.
		upstreamMode = jsonUpstreamModeEmpty
	case UpstreamModeParallel:
		upstreamMode = jsonUpstreamModeParallel
	case UpstreamModeFastestAddr:
		upstreamMode = jsonUpstreamModeFastestAddr
	}

	defPTRUps, err := s.defaultLocalPTRUpstreams(ctx)
	if err != nil {
		s.logger.ErrorContext(ctx, "getting local ptr upstreams", slogutil.KeyError, err)
	}

	return &jsonDNSConfig{
		Upstreams:                &upstreams,
		UpstreamsFile:            &upstreamFile,
		Bootstraps:               &bootstraps,
		Fallbacks:                &fallbacks,
		ProtectionEnabled:        &protectionEnabled,
		BlockingMode:             &blockingMode,
		BlockingIPv4:             blockingIPv4,
		BlockingIPv6:             blockingIPv6,
		Ratelimit:                &ratelimit,
		RatelimitSubnetLenIPv4:   &ratelimitSubnetLenIPv4,
		RatelimitSubnetLenIPv6:   &ratelimitSubnetLenIPv6,
		RatelimitWhitelist:       &ratelimitWhitelist,
		UpstreamTimeout:          &upstreamTimeout,
		EDNSCSCustomIP:           customIP,
		EDNSCSEnabled:            &enableEDNSClientSubnet,
		EDNSCSUseCustom:          &useCustom,
		DNSSECEnabled:            &enableDNSSEC,
		DisableIPv6:              &aaaaDisabled,
		BlockedResponseTTL:       &blockedResponseTTL,
		CacheEnabled:             &cacheEnabled,
		CacheSize:                &cacheSize,
		CacheMinTTL:              &cacheMinTTL,
		CacheMaxTTL:              &cacheMaxTTL,
		CacheOptimistic:          &cacheOptimistic,
		UpstreamMode:             &upstreamMode,
		ResolveClients:           &resolveClients,
		UsePrivateRDNS:           &usePrivateRDNS,
		LocalPTRUpstreams:        &localPTRUpstreams,
		DefaultLocalPTRUpstreams: defPTRUps,
		DisabledUntil:            protectionDisabledUntil,
	}
}

// defaultLocalPTRUpstreams returns the list of default local PTR resolvers
// filtered of AdGuard Home's own DNS server addresses.  It may appear empty.
func (s *Server) defaultLocalPTRUpstreams(ctx context.Context) (ups []string, err error) {
	matcher, err := s.conf.ourAddrsSet(ctx, s.logger)
	if err != nil {
		// Don't wrap the error because it's informative enough as is.
		return nil, err
	}

	sysResolvers := slices.DeleteFunc(slices.Clone(s.sysResolvers.Addrs()), matcher.Has)
	ups = make([]string, 0, len(sysResolvers))
	for _, r := range sysResolvers {
		ups = append(ups, r.String())
	}

	return ups, nil
}

// handleGetConfig handles requests to the GET /control/dns_info endpoint.
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	resp := s.getDNSConfig(ctx)
	aghhttp.WriteJSONResponseOK(ctx, s.logger, w, r, resp)
}

// checkBlockingMode returns an error if blocking mode is invalid.
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
	if req.BlockingMode == nil {
		return nil
	}

	return validateBlockingMode(*req.BlockingMode, req.BlockingIPv4, req.BlockingIPv6)
}

// checkUpstreamMode returns an error if the upstream mode is invalid.
func (req *jsonDNSConfig) checkUpstreamMode() (err error) {
	if req.UpstreamMode == nil {
		return nil
	}

	switch um := *req.UpstreamMode; um {
	case
		jsonUpstreamModeEmpty,
		jsonUpstreamModeLoadBalance,
		jsonUpstreamModeParallel,
		jsonUpstreamModeFastestAddr:
		return nil
	default:
		return fmt.Errorf("upstream_mode: incorrect value %q", um)
	}
}

// validate returns an error if any field of req is invalid.  l, ownAddrs,
// sysResolvers and privateNets must not be nil.
//
// TODO(s.chzhen):  Parse, don't validate.
func (req *jsonDNSConfig) validate(
	ctx context.Context,
	l *slog.Logger,
	ownAddrs addrPortSet,
	sysResolvers SystemResolvers,
	privateNets netutil.SubnetSet,
	curCacheSize uint32,
) (err error) {
	defer func() { err = errors.Annotate(err, "validating dns config: %w") }()

	err = req.validateUpstreamDNSServers(ctx, l, ownAddrs, sysResolvers, privateNets)
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	err = req.checkRatelimitSubnetMaskLen()
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	err = req.checkBlockingMode()
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	err = req.checkUpstreamMode()
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	err = req.validateCacheSettings(curCacheSize)
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	err = req.checkUpstreamTimeout()
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	return nil
}

// checkBootstrap returns an error if any bootstrap address is invalid.
func (req *jsonDNSConfig) checkBootstrap() (err error) {
	if req.Bootstraps == nil {
		return nil
	}

	var b string
	defer func() { err = errors.Annotate(err, "checking bootstrap %s: %w", b) }()

	for _, b = range *req.Bootstraps {
		if aghnet.IsCommentOrEmpty(b) {
			continue
		}

		var resolver *upstream.UpstreamResolver
		if resolver, err = upstream.NewUpstreamResolver(b, nil); err != nil {
			// Don't wrap the error because it's informative enough as is.
			return err
		}

		if err = resolver.Close(); err != nil {
			return fmt.Errorf("closing %s: %w", b, err)
		}
	}

	return nil
}

// containsPrivateRDNS returns true if req contains private RDNS settings and
// should be validated.
func (req *jsonDNSConfig) containsPrivateRDNS() (ok bool) {
	return (req.UsePrivateRDNS != nil && *req.UsePrivateRDNS) ||
		(req.LocalPTRUpstreams != nil && len(*req.LocalPTRUpstreams) > 0)
}

// checkPrivateRDNS returns an error if the configuration of the private RDNS is
// not valid.  l must not be nil.
func (req *jsonDNSConfig) checkPrivateRDNS(
	ctx context.Context,
	l *slog.Logger,
	ownAddrs addrPortSet,
	sysResolvers SystemResolvers,
	privateNets netutil.SubnetSet,
) (err error) {
	if !req.containsPrivateRDNS() {
		return nil
	}

	addrs := cmp.Or(req.LocalPTRUpstreams, &[]string{})

	uc, err := newPrivateConfig(
		ctx,
		l,
		*addrs,
		ownAddrs,
		sysResolvers,
		privateNets,
		&upstream.Options{
			Logger: slogutil.NewDiscardLogger(),
		},
	)
	err = errors.WithDeferred(err, uc.Close())
	if err != nil {
		return fmt.Errorf("private upstream servers: %w", err)
	}

	return nil
}

// validateUpstreamDNSServers returns an error if any field of req is invalid.
// l, ownAddrs, sysResolvers and privateNets must not be nil.
func (req *jsonDNSConfig) validateUpstreamDNSServers(
	ctx context.Context,
	l *slog.Logger,
	ownAddrs addrPortSet,
	sysResolvers SystemResolvers,
	privateNets netutil.SubnetSet,
) (err error) {
	var uc *proxy.UpstreamConfig
	opts := &upstream.Options{
		Logger: slogutil.NewDiscardLogger(),
	}

	if req.Upstreams != nil {
		uc, err = proxy.ParseUpstreamsConfig(*req.Upstreams, opts)
		err = errors.WithDeferred(err, uc.Close())
		if err != nil {
			return fmt.Errorf("upstream servers: %w", err)
		}
	}

	err = req.checkPrivateRDNS(ctx, l, ownAddrs, sysResolvers, privateNets)
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	err = req.checkBootstrap()
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	}

	if req.Fallbacks != nil {
		uc, err = proxy.ParseUpstreamsConfig(*req.Fallbacks, opts)
		err = errors.WithDeferred(err, uc.Close())
		if err != nil {
			return fmt.Errorf("fallback servers: %w", err)
		}
	}

	return nil
}

// validateCacheSettings returns an error if the cache configuration is invalid.
func (req *jsonDNSConfig) validateCacheSettings(curCacheSize uint32) (err error) {
	err = req.validateCacheSize(curCacheSize)
	if err != nil {
		// Don't wrap the error because it's informative enough as is.
		return err
	}

	if req.CacheMinTTL == nil && req.CacheMaxTTL == nil {
		return nil
	}

	var minTTL, maxTTL uint32
	if req.CacheMinTTL != nil {
		minTTL = *req.CacheMinTTL
	}

	if req.CacheMaxTTL != nil {
		maxTTL = *req.CacheMaxTTL
	}

	return validateCacheTTL(minTTL, maxTTL)
}

// validateCacheSize returns an error if the cache size configuration is
// invalid.  It also explicitly sets CacheEnabled to support legacy behavior.
func (req *jsonDNSConfig) validateCacheSize(curCacheSize uint32) (err error) {
	if req.CacheEnabled != nil && *req.CacheEnabled {
		size := curCacheSize
		if req.CacheSize != nil {
			size = *req.CacheSize
		}

		if size == 0 {
			return errors.Error("cache_size must be greater than zero when cache_enabled is true")
		}
	}

	if req.CacheEnabled == nil && req.CacheSize != nil {
		isEnabled := *req.CacheSize > 0
		req.CacheEnabled = &isEnabled
	}

	return nil
}

// checkRatelimitSubnetMaskLen returns an error if the length of the subnet mask
// for IPv4 or IPv6 addresses is invalid.
func (req *jsonDNSConfig) checkRatelimitSubnetMaskLen() (err error) {
	err = checkInclusion(req.RatelimitSubnetLenIPv4, 0, netutil.IPv4BitLen)
	if err != nil {
		return fmt.Errorf("ratelimit_subnet_len_ipv4 is invalid: %w", err)
	}

	err = checkInclusion(req.RatelimitSubnetLenIPv6, 0, netutil.IPv6BitLen)
	if err != nil {
		return fmt.Errorf("ratelimit_subnet_len_ipv6 is invalid: %w", err)
	}

	return nil
}

// checkUpstreamTimeout returns an error if the configuration of the upstream
// timeout is invalid.
func (req *jsonDNSConfig) checkUpstreamTimeout() (err error) {
	if req.UpstreamTimeout == nil {
		return nil
	}

	return validate.NoLessThan("upstream_timeout", *req.UpstreamTimeout, 1)
}

// checkInclusion returns an error if a ptr is not nil and points to value,
// that not in the inclusive range between minN and maxN.
func checkInclusion(ptr *uint, minN, maxN uint) (err error) {
	if ptr == nil {
		return nil
	}

	n := *ptr
	switch {
	case n < minN:
		return fmt.Errorf("value %d less than min %d", n, minN)
	case n > maxN:
		return fmt.Errorf("value %d greater than max %d", n, maxN)
	}

	return nil
}

// handleSetConfig handles requests to the POST /control/dns_config endpoint.
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	l := s.logger

	req := &jsonDNSConfig{}
	err := json.NewDecoder(r.Body).Decode(req)
	if err != nil {
		aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "decoding request: %s", err)

		return
	}

	// TODO(e.burkov):  Consider prebuilding this set on startup.
	ourAddrs, err := s.conf.ourAddrsSet(ctx, s.logger)
	if err != nil {
		// TODO(e.burkov):  Put into openapi.
		aghhttp.ErrorAndLog(
			ctx,
			l,
			r,
			w,
			http.StatusInternalServerError,
			"getting our addresses: %s",
			err,
		)

		return
	}

	err = req.validate(ctx, s.logger, ourAddrs, s.sysResolvers, s.privateNets, s.conf.CacheSize)
	if err != nil {
		aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "%s", err)

		return
	}

	restart := s.setConfig(req)
	s.conf.ConfModifier.Apply(ctx)

	if restart {
		err = s.Reconfigure(ctx, nil)
		if err != nil {
			aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusInternalServerError, "%s", err)
		}
	}
}

// setConfig sets the server parameters.  shouldRestart is true if the server
// should be restarted to apply changes.
func (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {
	s.serverLock.Lock()
	defer s.serverLock.Unlock()

	if dc.BlockingMode != nil {
		s.dnsFilter.SetBlockingMode(*dc.BlockingMode, dc.BlockingIPv4, dc.BlockingIPv6)
	}

	if dc.BlockedResponseTTL != nil {
		s.dnsFilter.SetBlockedResponseTTL(*dc.BlockedResponseTTL)
	}

	if dc.ProtectionEnabled != nil {
		s.dnsFilter.SetProtectionEnabled(*dc.ProtectionEnabled)
	}

	if dc.UpstreamMode != nil {
		s.conf.UpstreamMode = mustParseUpstreamMode(*dc.UpstreamMode)
	}

	if dc.EDNSCSUseCustom != nil && *dc.EDNSCSUseCustom {
		s.conf.EDNSClientSubnet.CustomIP = dc.EDNSCSCustomIP
	}

	setIfNotNil(&s.conf.EnableDNSSEC, dc.DNSSECEnabled)
	setIfNotNil(&s.conf.AAAADisabled, dc.DisableIPv6)

	return s.setConfigRestartable(dc)
}

// mustParseUpstreamMode returns an upstream mode parsed from jsonUpstreamMode.
// Panics in case of invalid value.
func mustParseUpstreamMode(mode jsonUpstreamMode) (um UpstreamMode) {
	switch mode {
	case jsonUpstreamModeEmpty, jsonUpstreamModeLoadBalance:
		return UpstreamModeLoadBalance
	case jsonUpstreamModeParallel:
		return UpstreamModeParallel
	case jsonUpstreamModeFastestAddr:
		return UpstreamModeFastestAddr
	default:
		// Should never happen, since the value should be validated.
		panic(fmt.Errorf("unexpected upstream mode: %q", mode))
	}
}

// setIfNotNil sets the value pointed at by currentPtr to the value pointed at
// by newPtr if newPtr is not nil.  currentPtr must not be nil.
func setIfNotNil[T any](currentPtr, newPtr *T) (hasSet bool) {
	if newPtr == nil {
		return false
	}

	*currentPtr = *newPtr

	return true
}

// setConfigRestartable sets the parameters which trigger a restart.
// shouldRestart is true if the server should be restarted to apply changes.
// s.serverLock is expected to be locked.
//
// TODO(a.garipov): Some of these could probably be updated without a restart.
// Inspect and consider refactoring.
func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
	for _, hasSet := range []bool{
		setIfNotNil(&s.conf.UpstreamDNS, dc.Upstreams),
		setIfNotNil(&s.conf.LocalPTRResolvers, dc.LocalPTRUpstreams),
		setIfNotNil(&s.conf.UpstreamDNSFileName, dc.UpstreamsFile),
		setIfNotNil(&s.conf.BootstrapDNS, dc.Bootstraps),
		setIfNotNil(&s.conf.FallbackDNS, dc.Fallbacks),
		setIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled),
		setIfNotNil(&s.conf.EDNSClientSubnet.UseCustom, dc.EDNSCSUseCustom),
		setIfNotNil(&s.conf.CacheEnabled, dc.CacheEnabled),
		setIfNotNil(&s.conf.CacheSize, dc.CacheSize),
		setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),
		setIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL),
		setIfNotNil(&s.conf.CacheOptimistic, dc.CacheOptimistic),
		setIfNotNil(&s.conf.AddrProcConf.UseRDNS, dc.ResolveClients),
		setIfNotNil(&s.conf.UsePrivateRDNS, dc.UsePrivateRDNS),
		setIfNotNil(&s.conf.RatelimitSubnetLenIPv4, dc.RatelimitSubnetLenIPv4),
		setIfNotNil(&s.conf.RatelimitSubnetLenIPv6, dc.RatelimitSubnetLenIPv6),
		setIfNotNil(&s.conf.RatelimitWhitelist, dc.RatelimitWhitelist),
	} {
		shouldRestart = shouldRestart || hasSet
		if shouldRestart {
			break
		}
	}

	if dc.Ratelimit != nil && s.conf.Ratelimit != *dc.Ratelimit {
		s.conf.Ratelimit = *dc.Ratelimit
		shouldRestart = true
	}

	if dc.UpstreamTimeout != nil {
		ut := time.Duration(*dc.UpstreamTimeout) * time.Second
		if s.conf.UpstreamTimeout != ut {
			s.conf.UpstreamTimeout = ut
			shouldRestart = true
		}
	}

	return shouldRestart
}

// upstreamJSON is a request body for handleTestUpstreamDNS endpoint.
type upstreamJSON struct {
	Upstreams        []string `json:"upstream_dns"`
	BootstrapDNS     []string `json:"bootstrap_dns"`
	FallbackDNS      []string `json:"fallback_dns"`
	PrivateUpstreams []string `json:"private_upstream"`
}

// closeBoots closes all the provided bootstrap servers and logs errors if any.
// l must not be nil.
func closeBoots(ctx context.Context, l *slog.Logger, boots []*upstream.UpstreamResolver) {
	for _, c := range boots {
		logCloserErr(ctx, c, "closing bootstrap", l.With("address", c.Address()))
	}
}

// handleTestUpstreamDNS handles requests to the POST /control/test_upstream_dns
// endpoint.
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	l := s.logger

	req := &upstreamJSON{}
	err := json.NewDecoder(r.Body).Decode(req)
	if err != nil {
		aghhttp.ErrorAndLog(
			ctx,
			l,
			r,
			w,
			http.StatusBadRequest,
			"Failed to read request body: %s",
			err,
		)

		return
	}

	opts := &upstream.Options{
		Logger:     aghslog.NewForUpstream(s.baseLogger, aghslog.UpstreamTypeTest),
		Timeout:    s.conf.UpstreamTimeout,
		PreferIPv6: s.conf.BootstrapPreferIPv6,
	}

	var boots []*upstream.UpstreamResolver
	opts.Bootstrap, boots, err = newBootstrap(req.BootstrapDNS, s.etcHosts, opts)
	if err != nil {
		aghhttp.ErrorAndLog(
			ctx,
			l,
			r,
			w,
			http.StatusBadRequest,
			"Failed to parse bootstrap servers: %s",
			err,
		)

		return
	}
	defer closeBoots(ctx, s.logger, boots)

	cv := newUpstreamConfigValidator(ctx, req.Upstreams, req.FallbackDNS, req.PrivateUpstreams, opts)
	cv.check(ctx, s.logger)
	cv.close()

	aghhttp.WriteJSONResponseOK(ctx, l, w, r, cv.status(ctx, l))
}

// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.
func (s *Server) handleCacheClear(w http.ResponseWriter, _ *http.Request) {
	s.dnsProxy.ClearCache()
	s.conf.ClientsContainer.ClearUpstreamCache()

	_, _ = io.WriteString(w, "OK")
}

// protectionJSON is an object for /control/protection endpoint.
type protectionJSON struct {
	Enabled  bool `json:"enabled"`
	Duration uint `json:"duration"`
}

// handleSetProtection is a handler for the POST /control/protection HTTP API.
func (s *Server) handleSetProtection(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	l := s.logger

	protectionReq := &protectionJSON{}
	err := json.NewDecoder(r.Body).Decode(protectionReq)
	if err != nil {
		aghhttp.ErrorAndLog(ctx, l, r, w, http.StatusBadRequest, "reading req: %s", err)

		return
	}

	var disabledUntil *time.Time
	if protectionReq.Duration > 0 {
		if protectionReq.Enabled {
			aghhttp.ErrorAndLog(
				ctx,
				l,
				r,
				w,
				http.StatusBadRequest,
				"Setting a duration is only allowed with protection disabling",
			)

			return
		}

		calcTime := time.Now().Add(time.Duration(protectionReq.Duration) * time.Millisecond)
		disabledUntil = &calcTime
	}

	func() {
		s.serverLock.Lock()
		defer s.serverLock.Unlock()

		s.dnsFilter.SetProtectionStatus(protectionReq.Enabled, disabledUntil)
	}()

	s.conf.ConfModifier.Apply(ctx)

	aghhttp.OK(ctx, l, w)
}

// registerHandlers registers web HTTP handlers.
func (s *Server) registerHandlers() {
	if webRegistered || s.conf.HTTPReg == nil {
		return
	}

	s.conf.HTTPReg.Register(http.MethodGet, "/control/dns_info", s.handleGetConfig)
	s.conf.HTTPReg.Register(http.MethodPost, "/control/dns_config", s.handleSetConfig)
	s.conf.HTTPReg.Register(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS)
	s.conf.HTTPReg.Register(http.MethodPost, "/control/protection", s.handleSetProtection)

	s.conf.HTTPReg.Register(http.MethodGet, "/control/access/list", s.handleAccessList)
	s.conf.HTTPReg.Register(http.MethodPost, "/control/access/set", s.handleAccessSet)

	s.conf.HTTPReg.Register(http.MethodPost, "/control/cache_clear", s.handleCacheClear)

	webRegistered = true
}
