package client

import (
	"fmt"
	"log/slog"
	"slices"
	"time"

	"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
	"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/stringutil"
	"github.com/AdguardTeam/golibs/timeutil"
)

// CommonUpstreamConfig contains common settings for custom client upstream
// configurations.
type CommonUpstreamConfig struct {
	Bootstrap               upstream.Resolver
	UpstreamTimeout         time.Duration
	BootstrapPreferIPv6     bool
	EDNSClientSubnetEnabled bool
	UseHTTP3Upstreams       bool
}

// customUpstreamConfig contains custom client upstream configuration and the
// timestamp of the latest configuration update.
type customUpstreamConfig struct {
	// proxyConf is the constructed upstream configuration for the [proxy],
	// derived from the fields below.  It is initialized on demand with
	// [newCustomUpstreamConfig].
	proxyConf *proxy.CustomUpstreamConfig

	// commonConfUpdate is the timestamp of the latest configuration update,
	// used to check against [upstreamManager.confUpdate] to determine if the
	// configuration is up to date.
	commonConfUpdate time.Time

	// upstreams is the cached list of custom upstream DNS servers used for the
	// configuration of proxyConf.
	upstreams []string

	// upstreamsCacheSize is the cached value of the cache size of the
	// upstreams, used for the configuration of proxyConf.
	upstreamsCacheSize uint32

	// upstreamsCacheEnabled is the cached value indicating whether the cache of
	// the upstreams is enabled for the configuration of proxyConf.
	upstreamsCacheEnabled bool

	// isChanged indicates whether the proxyConf needs to be updated.
	isChanged bool
}

// upstreamManager stores and updates custom client upstream configurations.
type upstreamManager struct {
	// logger is used for logging the operation of the upstream manager.  It
	// must not be nil.
	//
	// TODO(s.chzhen):  Consider using a logger with its own prefix.
	logger *slog.Logger

	// uidToCustomConf maps persistent client UID to the custom client upstream
	// configuration.  Stored UIDs must be in sync with the [index.uidToClient].
	uidToCustomConf map[UID]*customUpstreamConfig

	// commonConf is the common upstream configuration.
	commonConf *CommonUpstreamConfig

	// clock is used to get the current time.  It must not be nil.
	clock timeutil.Clock

	// confUpdate is the timestamp of the latest common upstream configuration
	// update.
	confUpdate time.Time
}

// newUpstreamManager returns the new properly initialized upstream manager.
func newUpstreamManager(logger *slog.Logger, clock timeutil.Clock) (m *upstreamManager) {
	return &upstreamManager{
		logger:          logger,
		uidToCustomConf: make(map[UID]*customUpstreamConfig),
		clock:           clock,
	}
}

// updateCommonUpstreamConfig updates the common upstream configuration and the
// timestamp of the latest configuration update.
func (m *upstreamManager) updateCommonUpstreamConfig(conf *CommonUpstreamConfig) {
	m.commonConf = conf
	m.confUpdate = m.clock.Now()
}

// updateCustomUpstreamConfig updates the stored custom client upstream
// configuration associated with the persistent client.  It also sets
// [customUpstreamConfig.isChanged] to true so [customUpstreamConfig.proxyConf]
// can be updated later in [upstreamManager.customUpstreamConfig].
func (m *upstreamManager) updateCustomUpstreamConfig(c *Persistent) {
	cliConf, ok := m.uidToCustomConf[c.UID]
	if !ok {
		cliConf = &customUpstreamConfig{
			commonConfUpdate: m.confUpdate,
		}

		m.uidToCustomConf[c.UID] = cliConf
	}

	// TODO(s.chzhen):  Compare before cloning.
	cliConf.upstreams = slices.Clone(c.Upstreams)
	cliConf.upstreamsCacheSize = c.UpstreamsCacheSize
	cliConf.upstreamsCacheEnabled = c.UpstreamsCacheEnabled
	cliConf.isChanged = true
}

// customUpstreamConfig returns the custom client upstream configuration.
func (m *upstreamManager) customUpstreamConfig(uid UID) (proxyConf *proxy.CustomUpstreamConfig) {
	cliConf, ok := m.uidToCustomConf[uid]
	if !ok {
		// TODO(s.chzhen):  Consider panic.
		m.logger.Error("no associated custom client upstream config")

		return nil
	}

	if !m.isConfigChanged(cliConf) {
		return cliConf.proxyConf
	}

	if cliConf.proxyConf != nil {
		err := cliConf.proxyConf.Close()
		if err != nil {
			// TODO(s.chzhen):  Pass context.
			m.logger.Debug("closing custom upstream config", slogutil.KeyError, err)
		}
	}

	proxyConf = newCustomUpstreamConfig(cliConf, m.commonConf)
	cliConf.proxyConf = proxyConf
	cliConf.isChanged = false

	return proxyConf
}

// isConfigChanged returns true if the update is necessary for the custom client
// upstream configuration.
func (m *upstreamManager) isConfigChanged(cliConf *customUpstreamConfig) (ok bool) {
	return !m.confUpdate.Equal(cliConf.commonConfUpdate) || cliConf.isChanged
}

// clearUpstreamCache clears the upstream cache for each stored custom client
// upstream configuration.
func (m *upstreamManager) clearUpstreamCache() {
	for _, c := range m.uidToCustomConf {
		if c.proxyConf != nil {
			c.proxyConf.ClearCache()
		}
	}
}

// remove deletes the custom client upstream configuration and closes
// [customUpstreamConfig.proxyConf] if necessary.
func (m *upstreamManager) remove(uid UID) (err error) {
	cliConf, ok := m.uidToCustomConf[uid]
	if !ok {
		// TODO(s.chzhen):  Consider panic.
		return errors.Error("no associated custom client upstream config")
	}

	delete(m.uidToCustomConf, uid)

	if cliConf.proxyConf != nil {
		return cliConf.proxyConf.Close()
	}

	return nil
}

// close shuts down each stored custom client upstream configuration.
func (m *upstreamManager) close() (err error) {
	var errs []error
	for _, c := range m.uidToCustomConf {
		if c.proxyConf == nil {
			continue
		}

		errs = append(errs, c.proxyConf.Close())
	}

	return errors.Join(errs...)
}

// newCustomUpstreamConfig returns the new properly initialized custom proxy
// upstream configuration for the client.
func newCustomUpstreamConfig(
	cliConf *customUpstreamConfig,
	conf *CommonUpstreamConfig,
) (proxyConf *proxy.CustomUpstreamConfig) {
	upstreams := stringutil.FilterOut(cliConf.upstreams, aghnet.IsCommentOrEmpty)
	if len(upstreams) == 0 {
		return nil
	}

	upsConf, err := proxy.ParseUpstreamsConfig(
		upstreams,
		&upstream.Options{
			Bootstrap:    conf.Bootstrap,
			Timeout:      time.Duration(conf.UpstreamTimeout),
			HTTPVersions: aghnet.UpstreamHTTPVersions(conf.UseHTTP3Upstreams),
			PreferIPv6:   conf.BootstrapPreferIPv6,
		},
	)
	if err != nil {
		// Should not happen because upstreams are already validated.  See
		// [Persistent.validate].
		panic(fmt.Errorf("creating custom upstream config: %w", err))
	}

	return proxy.NewCustomUpstreamConfig(
		upsConf,
		cliConf.upstreamsCacheEnabled,
		int(cliConf.upstreamsCacheSize),
		conf.EDNSClientSubnetEnabled,
	)
}