// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package local

import (
	"context"
	"time"

	"github.com/hashicorp/go-hclog"

	"github.com/hashicorp/consul/agent/local"
	"github.com/hashicorp/consul/agent/proxycfg"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/agent/token"
)

const resyncFrequency = 30 * time.Second

const source proxycfg.ProxySource = "local"

// SyncConfig contains the dependencies required by Sync.
type SyncConfig struct {
	// Manager is the proxycfg Manager with which proxy services will be registered.
	Manager ConfigManager

	// State is the agent's local state that will be watched for proxy registrations.
	State *local.State

	// Tokens is used to retrieve a fallback ACL token if a service is registered
	// without one.
	Tokens *token.Store

	// NodeName is the name of the local agent node.
	NodeName string

	// NodeLocality
	NodeLocality *structs.Locality

	// Logger will be used to write log messages.
	Logger hclog.Logger

	// ResyncFrequency is how often to do a resync and recreate any terminated
	// watches.
	ResyncFrequency time.Duration
}

// Sync watches the agent's local state and registers/deregisters services with
// the proxycfg Manager ahead-of-time so they're ready immediately when a proxy
// begins an xDS stream.
//
// It runs until the given context is canceled, so should be called it its own
// goroutine.
//
// Note: proxy service definitions from the agent's local state will always
// overwrite definitions of the same service from other sources (e.g. the
// catalog).
func Sync(ctx context.Context, cfg SyncConfig) {
	// Single item buffer is enough since there is no data transferred so this is
	// "level triggering" and we can't miss actual data.
	stateCh := make(chan struct{}, 1)

	cfg.State.Notify(stateCh)
	defer cfg.State.StopNotify(stateCh)

	var resyncCh <-chan time.Time
	for {
		sync(cfg)

		if resyncCh == nil && cfg.ResyncFrequency > 0 {
			resyncCh = time.After(cfg.ResyncFrequency)
		}

		select {
		case <-stateCh:
			// Wait for a state change.
		case <-resyncCh:
			resyncCh = nil
		case <-ctx.Done():
			return
		}
	}
}

func sync(cfg SyncConfig) {
	cfg.Logger.Trace("syncing proxy services from local state")

	services := cfg.State.AllServices()

	// Traverse the local state and ensure all proxy services are registered
	for sid, svc := range services {
		if !svc.Kind.IsProxy() {
			continue
		}

		// Retrieve the token used to register the service, or fallback to the
		// default user token. This token is expected to match the token used in
		// the xDS request for this data.
		token := cfg.State.ServiceToken(sid)
		if token == "" {
			token = cfg.Tokens.UserToken()
		}

		id := proxycfg.ProxyID{
			ServiceID: sid,
			NodeName:  cfg.NodeName,

			// Note: we *intentionally* don't set Token here. All watches on local
			// services use the same ACL token, regardless of whatever token is
			// presented in the xDS stream (the token presented to the xDS server
			// is checked before the watch is created).
			Token: "",
		}

		// We inherit the node's locality at runtime (not persisted).
		// The service locality takes precedence if it was set directly during
		// registration.
		svc = svc.DeepCopy()
		if svc.Locality == nil {
			svc.Locality = cfg.NodeLocality
		}

		// TODO(banks): need to work out when to default some stuff. For example
		// Proxy.LocalServicePort is practically necessary for any sidecar and can
		// default to the port of the sidecar service, but only if it's already
		// registered and once we get past here, we don't have enough context to
		// know that so we'd need to set it here if not during registration of the
		// proxy service. Sidecar Service in the interim can do that, but we should
		// validate more generally that that is always true.
		err := cfg.Manager.Register(id, svc, source, token, true)
		if err != nil {
			cfg.Logger.Error("failed to watch proxy service",
				"service", sid.String(),
				"error", err,
			)
		}
	}

	// Now see if any proxies were removed
	for _, proxyID := range cfg.Manager.RegisteredProxies(source) {
		if _, ok := services[proxyID.ServiceID]; !ok {
			cfg.Manager.Deregister(proxyID, source)
		}
	}
}

//go:generate mockery --name ConfigManager --inpackage
type ConfigManager interface {
	Watch(id proxycfg.ProxyID) (<-chan *proxycfg.ConfigSnapshot, context.CancelFunc)
	Register(proxyID proxycfg.ProxyID, service *structs.NodeService, source proxycfg.ProxySource, token string, overwrite bool) error
	Deregister(proxyID proxycfg.ProxyID, source proxycfg.ProxySource)
	RegisteredProxies(source proxycfg.ProxySource) []proxycfg.ProxyID
}
