// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022 Datadog, Inc.

package remoteconfig

import (
	"bytes"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"maps"
	"math/big"
	"net/http"
	"reflect"
	"slices"
	"sync"
	"time"

	"github.com/DataDog/dd-trace-go/v2/internal"
	"github.com/DataDog/dd-trace-go/v2/internal/log"
	"github.com/DataDog/dd-trace-go/v2/internal/processtags"

	rc "github.com/DataDog/datadog-agent/pkg/remoteconfig/state"
)

// Callback represents a function that can process a remote config update.
// A Callback function can be registered to a remote config client to automatically
// react upon receiving updates. This function returns the configuration processing status
// for each config file received through the update.
type Callback func(updates map[string]ProductUpdate) map[string]rc.ApplyStatus

// ProductCallback is like Callback but for a specific product.
type ProductCallback func(update ProductUpdate) map[string]rc.ApplyStatus

// Capability represents a bit index to be set in clientData.Capabilites in
// order to register a client for a specific capability. These bit indexes
// correspond to the Remote Config specification, see
// https://github.com/DataDog/dd-source/blob/9b29208565b6e9c9644d8488520a24eb252ca1cb/domains/remote-config/shared/libs/rc/capabilities.go#L28
type Capability uint

const (
	_ Capability = iota
	// ASMActivation represents the capability to activate ASM through remote configuration
	ASMActivation
	// ASMIPBlocking represents the capability for ASM to block requests based on user IP
	ASMIPBlocking
	// ASMDDRules represents the capability to update the rules used by the ASM WAF for threat detection
	ASMDDRules
	// ASMExclusions represents the capability for ASM to exclude traffic from its protections
	ASMExclusions
	// ASMRequestBlocking represents the capability for ASM to block requests based on the HTTP request related WAF addresses
	ASMRequestBlocking
	// ASMResponseBlocking represents the capability for ASM to block requests based on the HTTP response related WAF addresses
	ASMResponseBlocking
	// ASMUserBlocking represents the capability for ASM to block requests based on user ID
	ASMUserBlocking
	// ASMCustomRules represents the capability for ASM to receive and use user-defined security rules
	ASMCustomRules
	// ASMCustomBlockingResponse represents the capability for ASM to receive and use user-defined blocking responses
	ASMCustomBlockingResponse
	// ASMTrustedIPs represents Trusted IPs through the ASM product
	ASMTrustedIPs
	// ASMApiSecuritySampleRate represents API Security sampling rate
	ASMApiSecuritySampleRate
	// APMTracingSampleRate represents the rate at which to sample traces from APM client libraries
	APMTracingSampleRate
	// APMTracingLogsInjection enables APM client libraries to inject trace ids into log records
	APMTracingLogsInjection
	// APMTracingHTTPHeaderTags enables APM client libraries to tag http header values to http server or client spans
	APMTracingHTTPHeaderTags
	// APMTracingCustomTags enables APM client to set custom tags on all spans
	APMTracingCustomTags
	// ASMProcessorOverrides adds support for processor overrides through the ASM RC Product
	ASMProcessorOverrides
	// ASMCustomDataScanners adds support for custom data scanners through the ASM RC Product
	ASMCustomDataScanners
	// ASMExclusionData adds support configurable exclusion filter data from the ASM_DATA Product
	ASMExclusionData
	// APMTracingEnabled enables APM tracing
	APMTracingEnabled
	// APMTracingDataStreamsEnabled enables Data Streams Monitoring
	APMTracingDataStreamsEnabled
	// ASMRASPSQLI enables ASM support for runtime protection against SQL Injection attacks
	ASMRASPSQLI
	// ASMRASPLFI enables ASM support for runtime protection against Local File Inclusion attacks
	ASMRASPLFI
	// ASMRASPSSRF enables ASM support for runtime protection against SSRF attacks
	ASMRASPSSRF
	// ASMRASPSHI enables ASM support for runtime protection against XSS attacks
	ASMRASPSHI
	// ASMRASPXXE enables ASM support for runtime protection against XXE attacks
	ASMRASPXXE
	// ASMRASPRCE enables ASM support for runtime protection against Remote Code Execution
	ASMRASPRCE
	// ASMRASPNOSQLI enables ASM support for runtime protection against NoSQL Injection attacks
	ASMRASPNOSQLI
	// ASMRASPXSS enables ASM support for runtime protection against Cross Site Scripting attacks
	ASMRASPXSS
	// APMTracingSampleRules represents the sampling rate using matching rules from APM client libraries
	APMTracingSampleRules
	// CSMActivation represents the capability to activate CSM through remote configuration
	CSMActivation
	// ASMAutoUserInstrumMode represents the capability to enable the automatic user instrumentation mode
	ASMAutoUserInstrumMode
	// ASMEndpointFingerprinting represents the capability to enable endpoint fingerprinting
	ASMEndpointFingerprinting
	// ASMSessionFingerprinting represents the capability to enable session fingerprinting
	ASMSessionFingerprinting
	// ASMNetworkFingerprinting represents the capability to enable network fingerprinting
	ASMNetworkFingerprinting
	// ASMHeaderFingerprinting represents the capability to enable header fingerprinting
	ASMHeaderFingerprinting
	// ASMTruncationRules is the support for truncation payload rules
	ASMTruncationRules
	// ASMRASPCommandInjection represents the capability for ASM's RASP Command Injection prevention
	ASMRASPCommandInjection
	// APMTracingEnableDynamicInstrumentation represents the capability to enable dynamic instrumentation
	APMTracingEnableDynamicInstrumentation
	// APMTracingEnableExceptionReplay represents the capability to enable exception replay
	APMTracingEnableExceptionReplay
	// APMTracingEnableCodeOrigin represents the capability to enable code origin
	APMTracingEnableCodeOrigin
	// APMTracingEnableLiveDebugging represents the capability to enable live debugging
	APMTracingEnableLiveDebugging
	// ASMDDMultiConfig represents the capability to handle multiple ASM_DD configuration objects
	ASMDDMultiConfig
	// ASMTraceTaggingRules represents the capability to honor trace tagging rules
	ASMTraceTaggingRules
	ASMExtendedDataCollection
	// APMTracingMulticonfig is the capability to handle cascading configs for the
	// APMTracing product.
	APMTracingMulticonfig
	// FFEFlagEvaluation represents the capability for feature flag evaluation via OpenFeature
	FFEFlagEvaluation
)

// ErrClientNotStarted is returned when the remote config client is not started.
var ErrClientNotStarted = errors.New("remote config client not started")

// ProductUpdate represents an update for a specific product.
// It is a map of file path to raw file content
type ProductUpdate map[string][]byte

// A Client interacts with an Agent to update and track the state of remote
// configuration
type Client struct {
	sync.RWMutex
	ClientConfig

	clientID   string
	endpoint   string
	repository *rc.Repository
	stop       chan struct{}

	// When acquiring several locks and using defer to release them, make sure to acquire the locks in the following order:
	callbacks       []Callback
	_callbacksMu    sync.RWMutex
	products        map[string]struct{}
	productsMu      sync.RWMutex
	subscriptionsMu struct {
		sync.RWMutex
		subs        []subscription
		idAllocator int
	}
	// capabilities contains the capabilities that have been registered through
	// RegisterCapability. The capabilities of the subscribers added through
	// RegisterSubscribers are reflected inside subscriptions.
	capabilities   map[Capability]struct{}
	capabilitiesMu sync.RWMutex

	lastError        error
	lastConfigStates []*configState
}

// subscription represents a callback that was registered to run on updates to a
// specific product.
type subscription struct {
	// id uniquely identifies this subscription. It is used to remove a specific
	// subscription.
	id           int
	product      string
	capabilities []Capability
	callback     ProductCallback
}

var (
	// client is a RC client singleton that can be accessed by multiple products (tracing, ASM, profiling etc.).
	// Using a single RC client instance in the tracer is a requirement for remote configuration.
	client    *Client
	clientMux sync.Mutex
	started   bool
)

// newClient creates a new remoteconfig Client
func newClient(config ClientConfig) (*Client, error) {
	repo, err := rc.NewUnverifiedRepository()
	if err != nil {
		return nil, err
	}
	if config.HTTP == nil {
		config.HTTP = DefaultClientConfig().HTTP
	}

	return &Client{
		ClientConfig: config,
		clientID:     generateID(),
		endpoint:     fmt.Sprintf("%s/v0.7/config", config.AgentURL),
		repository:   repo,
		stop:         make(chan struct{}),
		lastError:    nil,
		callbacks:    []Callback{},
		capabilities: map[Capability]struct{}{},
		products:     map[string]struct{}{},
	}, nil
}

// Start starts the client's update poll loop in a fresh goroutine.
// Noop if the client has already started.
func Start(config ClientConfig) error {
	if !internal.BoolEnv("DD_REMOTE_CONFIGURATION_ENABLED", true) {
		// Don't start polling if the feature is disabled explicitly
		return nil
	}
	clientMux.Lock()
	defer clientMux.Unlock()

	if started {
		// Return early if already started.
		return nil
	}
	var err error
	client, err = newClient(config)
	if err != nil {
		return err
	}
	started = true

	var (
		pollInterval = client.PollInterval
		stop         = client.stop
	)
	go func() {
		ticker := time.NewTicker(pollInterval)
		defer ticker.Stop()

		for {
			select {
			case <-stop:
				close(stop)
				return
			case <-ticker.C:
				if client == nil {
					return
				}
				client.Lock()
				client.updateState()
				client.Unlock()
			}
		}
	}()
	return nil
}

// Stop stops the client's update poll loop.
// Noop if the client has already been stopped.
// The remote config client is supposed to have the same lifecycle as the tracer.
// It can't be restarted after a call to Stop() unless explicitly calling Reset().
func Stop() {
	clientMux.Lock()
	defer clientMux.Unlock()

	if client == nil {
		// In case Stop() is called before Start()
		return
	}
	if !started {
		// Return early if already stopped.
		return
	}
	log.Debug("remoteconfig: gracefully stopping the client")
	client.stop <- struct{}{}
	select {
	case <-client.stop:
		log.Debug("remoteconfig: client stopped successfully")
	case <-time.After(time.Second):
		log.Debug("remoteconfig: client stopping timeout")
	}
	client = nil
	started = false
}

// Reset destroys the client instance.
// To be used only in tests to reset the state of the client.
func Reset() {
	clientMux.Lock()
	defer clientMux.Unlock()

	client = nil
	started = false
}

func (c *Client) updateState() {
	data, err := c.newUpdateRequest()
	if err != nil {
		log.Error("remoteconfig: unexpected error while creating a new update request payload: %s", err.Error())
		return
	}

	req, err := http.NewRequest(http.MethodGet, c.endpoint, &data)
	if err != nil {
		log.Error("remoteconfig: unexpected error while creating a new http request: %s", err.Error())
		return
	}
	if internal.ContainerID() != "" {
		req.Header.Set("Datadog-Container-ID", internal.ContainerID())
	}
	if internal.EntityID() != "" {
		req.Header.Set("Datadog-Entity-ID", internal.EntityID())
	}

	resp, err := c.HTTP.Do(req)
	if err != nil {
		log.Debug("remoteconfig: http request error: %s", err.Error())
		return
	}
	// Flush and close the response body when returning (cf. https://pkg.go.dev/net/http#Client.Do)
	defer func() {
		io.ReadAll(resp.Body)
		resp.Body.Close()
	}()

	if sc := resp.StatusCode; sc != http.StatusOK {
		log.Debug("remoteconfig: http request error: response status code is not 200 (OK) but %s", http.StatusText(sc))
		return
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Error("remoteconfig: http request error: could not read the response body: %s", err.Error())
		return
	}

	if body := string(respBody); body == `{}` || body == `null` {
		return
	}

	var update clientGetConfigsResponse
	if err := json.Unmarshal(respBody, &update); err != nil {
		log.Error("remoteconfig: http request error: could not parse the json response body: %s", err.Error())
		return
	}

	// Skip update if there's no new TUF metadata to prevent targets_version from being reset to 0.
	// When the Remote Config server sends a response with no new TUF targets, calling repository.Update() with
	// empty targets must not cause the targets version to reset.
	if len(update.Targets) == 0 {
		log.Debug("remoteconfig: skipping update with no TUF metadata (empty targets)")
		return
	}

	c.lastError = c.applyUpdate(&update)
}

type SubscriptionToken int

// Subscribe registers a product and its callback to be invoked when the client
// receives configuration updates for the specified product. The returned token
// can be passed to Unsubscribe to remove the subscription.
//
// It is legal to call Subscribe multiple times with the same product, and even
// to call it multiple times with the same product and different capabilities.
// Note, however, that the capabilities reported to RC are the union of all
// capabilities passed to Subscribe and RegisterCapability (across all
// products); in other words, the capabilities are not tied to a specific
// product or subscription.
//
// Subscribe should be preferred over RegisterProduct and RegisterCallback if
// your callback only handles a single product.
func Subscribe(product string, callback ProductCallback, capabilities ...Capability) (SubscriptionToken, error) {
	if client == nil {
		return 0, ErrClientNotStarted
	}
	client.productsMu.RLock()
	defer client.productsMu.RUnlock()
	if _, found := client.products[product]; found {
		return 0, fmt.Errorf("product %s already registered via RegisterProduct", product)
	}

	client.subscriptionsMu.Lock()
	defer client.subscriptionsMu.Unlock()
	client.subscriptionsMu.idAllocator++
	id := client.subscriptionsMu.idAllocator
	sub := subscription{
		id:           id,
		product:      product,
		capabilities: capabilities,
		callback:     callback,
	}
	client.subscriptionsMu.subs = append(client.subscriptionsMu.subs, sub)

	client.capabilitiesMu.Lock()
	defer client.capabilitiesMu.Unlock()
	for _, cap := range capabilities {
		client.capabilities[cap] = struct{}{}
	}
	return SubscriptionToken(id), nil
}

// Unsubscribe removes a subscription previously registered via Subscribe. The
// capabilities associated with that subscription will no longer be reported to
// RC.
func Unsubscribe(token SubscriptionToken) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client.subscriptionsMu.Lock()
	defer client.subscriptionsMu.Unlock()
	for i, sub := range client.subscriptionsMu.subs {
		if sub.id == int(token) {
			client.subscriptionsMu.subs = append(client.subscriptionsMu.subs[:i], client.subscriptionsMu.subs[i+1:]...)
			return nil
		}
	}
	return fmt.Errorf("subscription %d not found", token)
}

// RegisterCallback allows registering a callback that will be invoked when the client
// receives configuration updates. It is up to that callback to then decide what to do
// depending on the product related to the configuration update.
func RegisterCallback(f Callback) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client._callbacksMu.Lock()
	defer client._callbacksMu.Unlock()
	client.callbacks = append(client.callbacks, f)
	return nil
}

// UnregisterCallback removes a previously registered callback from the active callbacks list
// This remove operation preserves ordering
func UnregisterCallback(f Callback) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client._callbacksMu.Lock()
	defer client._callbacksMu.Unlock()

	toRemove := reflect.ValueOf(f).Pointer()
	client.callbacks = slices.DeleteFunc(client.callbacks, func(cb Callback) bool {
		return reflect.ValueOf(cb).Pointer() == toRemove
	})
	return nil
}

// RegisterProduct adds a product to the list of products listened by the client
func RegisterProduct(p string) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client.productsMu.Lock()
	defer client.productsMu.Unlock()
	client.subscriptionsMu.RLock()
	defer client.subscriptionsMu.RUnlock()
	for _, s := range client.subscriptionsMu.subs {
		if s.product == p {
			return fmt.Errorf("product %s already registered via Subscribe", p)
		}
	}

	client.products[p] = struct{}{}
	return nil
}

// UnregisterProduct removes a product from the list of products listened by the client
func UnregisterProduct(p string) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client.productsMu.Lock()
	defer client.productsMu.Unlock()
	delete(client.products, p)
	return nil
}

// HasProduct returns whether a given product was registered
func HasProduct(p string) (bool, error) {
	if client == nil {
		return false, ErrClientNotStarted
	}
	client.productsMu.RLock()
	defer client.productsMu.RUnlock()

	_, found := client.products[p]
	if found {
		return true, nil
	}

	client.subscriptionsMu.RLock()
	defer client.subscriptionsMu.RUnlock()
	for _, s := range client.subscriptionsMu.subs {
		if s.product == p {
			return true, nil
		}
	}

	return false, nil
}

// RegisterCapability adds a capability to the list of capabilities exposed by the client when requesting
// configuration updates
func RegisterCapability(cpb Capability) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client.capabilitiesMu.Lock()
	defer client.capabilitiesMu.Unlock()
	client.capabilities[cpb] = struct{}{}
	return nil
}

// UnregisterCapability removes a capability from the list of capabilities exposed by the client when requesting
// configuration updates
func UnregisterCapability(cpb Capability) error {
	if client == nil {
		return ErrClientNotStarted
	}
	client.capabilitiesMu.Lock()
	defer client.capabilitiesMu.Unlock()
	delete(client.capabilities, cpb)
	return nil
}

// HasCapability returns whether a given capability was registered
func HasCapability(cpb Capability) (bool, error) {
	if client == nil {
		return false, ErrClientNotStarted
	}
	client.capabilitiesMu.RLock()
	defer client.capabilitiesMu.RUnlock()
	_, found := client.capabilities[cpb]
	return found, nil
}

func (c *Client) allCapabilities() *big.Int {
	capa := big.NewInt(0)

	// Read registered capabilities without holding the lock while we also read subscriptions.
	c.capabilitiesMu.RLock()
	for i := range c.capabilities {
		capa.SetBit(capa, int(i), 1)
	}
	c.capabilitiesMu.RUnlock()

	c.subscriptionsMu.RLock()
	for _, s := range c.subscriptionsMu.subs {
		for _, cap := range s.capabilities {
			capa.SetBit(capa, int(cap), 1)
		}
	}
	c.subscriptionsMu.RUnlock()

	return capa
}

func (c *Client) globalCallbacks() []Callback {
	c._callbacksMu.RLock()
	defer c._callbacksMu.RUnlock()
	callbacks := make([]Callback, len(c.callbacks))
	copy(callbacks, c.callbacks)
	return callbacks
}

func (c *Client) productCallbacks() map[string]ProductCallback {
	c.subscriptionsMu.RLock()
	defer c.subscriptionsMu.RUnlock()
	callbacks := make(map[string]ProductCallback, len(c.subscriptionsMu.subs))
	for _, v := range c.subscriptionsMu.subs {
		callbacks[v.product] = v.callback
	}
	return callbacks
}

func (c *Client) allProducts() []string {
	c.productsMu.RLock()
	defer c.productsMu.RUnlock()
	c.subscriptionsMu.RLock()
	defer c.subscriptionsMu.RUnlock()

	// Dedup products across all subscriptions and registered products.
	ps := make(map[string]struct{}, len(c.products)+len(c.subscriptionsMu.subs))

	for p := range c.products {
		ps[p] = struct{}{}
	}
	for _, s := range c.subscriptionsMu.subs {
		ps[s.product] = struct{}{}
	}

	products := make([]string, 0, len(ps))
	for p := range ps {
		products = append(products, p)
	}

	return products
}

func (c *Client) applyUpdate(pbUpdate *clientGetConfigsResponse) error {
	fileMap := make(map[string][]byte, len(pbUpdate.TargetFiles))
	allProducts := c.allProducts()
	productUpdates := make(map[string]ProductUpdate, len(allProducts))
	for _, f := range pbUpdate.TargetFiles {
		path, valid := ParsePath(f.Path)
		if !valid {
			log.Warn("remoteconfig: ignoring invalid target file path: %s", f.Path)
			continue
		}

		fileMap[f.Path] = f.Raw
		if !slices.Contains(allProducts, path.Product) {
			log.Debug("remoteconfig: received file for unknown product %s (known: %#v): %s", path.Product, allProducts, f.Path) //nolint:gocritic // Debug logging for unknown products
		}
		if productUpdates[path.Product] == nil {
			productUpdates[path.Product] = make(ProductUpdate)
		}
		productUpdates[path.Product][f.Path] = f.Raw
	}

	mapify := func(s *rc.RepositoryState) map[string]string {
		m := make(map[string]string)
		for i := range s.Configs {
			path := s.CachedFiles[i].Path
			product := s.Configs[i].Product
			m[path] = product
		}
		return m
	}

	// Check the repository state before and after the update to detect which configs are not being sent anymore.
	// This is needed because some products can stop sending configurations, and we want to make sure that the subscribers
	// are provided with this information in this case
	stateBefore, err := c.repository.CurrentState()
	if err != nil {
		return fmt.Errorf("repository current state error: %s", err)
	}
	products, err := c.repository.Update(rc.Update{
		TUFRoots:      pbUpdate.Roots,
		TUFTargets:    pbUpdate.Targets,
		TargetFiles:   fileMap,
		ClientConfigs: pbUpdate.ClientConfigs,
	})
	if err != nil {
		return fmt.Errorf("repository update error: %s", err)
	}
	stateAfter, err := c.repository.CurrentState()
	if err != nil {
		return fmt.Errorf("repository current state error after update: %s", err)
	}

	// Save the config states after successful update for use in error reporting
	configStates := make([]*configState, 0, len(stateAfter.Configs))
	for _, f := range stateAfter.Configs {
		configStates = append(configStates, &configState{
			ID:         f.ID,
			Version:    f.Version,
			Product:    f.Product,
			ApplyState: f.ApplyStatus.State,
			ApplyError: f.ApplyStatus.Error,
		})
	}
	c.lastConfigStates = configStates

	// Create a config files diff between before/after the update to see which config files are missing
	mBefore := mapify(&stateBefore)
	for k := range mapify(&stateAfter) {
		delete(mBefore, k)
	}

	// Set the payload data to nil for missing config files. The callbacks then can handle the nil config case to detect
	// that this config will not be updated anymore.
	updatedProducts := make(map[string]struct{})
	for path, product := range mBefore {
		if productUpdates[product] == nil {
			productUpdates[product] = make(ProductUpdate)
		}
		productUpdates[product][path] = nil
		updatedProducts[product] = struct{}{}
	}
	// Aggregate updated products and missing products so that callbacks get called for both
	for _, p := range products {
		updatedProducts[p] = struct{}{}
	}

	if len(updatedProducts) == 0 {
		return nil
	}
	// Performs the callbacks registered and update the application status in the repository (RCTE2)
	// In case of several callbacks handling the same config, statuses take precedence in this order:
	// 1 - ApplyStateError
	// 2 - ApplyStateUnacknowledged
	// 3 - ApplyStateAcknowledged
	// This makes sure that any product that would need to re-receive the config in a subsequent update will be allowed to
	statuses := make(map[string]rc.ApplyStatus)
	for _, cb := range c.globalCallbacks() {
		for path, status := range cb(productUpdates) {
			if s, ok := statuses[path]; !ok || status.State == rc.ApplyStateError ||
				s.State == rc.ApplyStateAcknowledged && status.State == rc.ApplyStateUnacknowledged {
				statuses[path] = status
			}
		}
	}
	// Call the product-specific callbacks registered via Subscribe
	productCallbacks := c.productCallbacks()
	for product, update := range productUpdates {
		if fn, ok := productCallbacks[product]; ok {
			maps.Copy(statuses, fn(update))
		}
	}
	for p, s := range statuses {
		c.repository.UpdateApplyStatus(p, s)
	}

	return nil
}

func (c *Client) newUpdateRequest() (bytes.Buffer, error) {
	state, err := c.repository.CurrentState()
	if err != nil {
		return bytes.Buffer{}, err
	}
	// Temporary check while using untrusted repo, for which no initial root file is provided
	if state.RootsVersion < 1 {
		state.RootsVersion = 1
	}

	pbCachedFiles := make([]*targetFileMeta, 0, len(state.CachedFiles))
	for _, f := range state.CachedFiles {
		pbHashes := make([]*targetFileHash, 0, len(f.Hashes))
		for alg, hash := range f.Hashes {
			pbHashes = append(pbHashes, &targetFileHash{
				Algorithm: alg,
				Hash:      hex.EncodeToString(hash),
			})
		}
		pbCachedFiles = append(pbCachedFiles, &targetFileMeta{
			Path:   f.Path,
			Length: int64(f.Length),
			Hashes: pbHashes,
		})
	}

	hasError := c.lastError != nil
	errMsg := ""
	if hasError {
		errMsg = c.lastError.Error()
	}

	// When there's an error, use the last known good config states
	// Otherwise, use the current state and also update lastConfigStates
	var pbConfigState []*configState
	if hasError && c.lastConfigStates != nil {
		// Use the last successfully retrieved config states during error state
		pbConfigState = c.lastConfigStates
	} else {
		// No error, build config states from current repository state
		pbConfigState = make([]*configState, 0, len(state.Configs))
		for _, f := range state.Configs {
			pbConfigState = append(pbConfigState, &configState{
				ID:         f.ID,
				Version:    f.Version,
				Product:    f.Product,
				ApplyState: f.ApplyStatus.State,
				ApplyError: f.ApplyStatus.Error,
			})
		}
		// Also update lastConfigStates for future use
		c.lastConfigStates = pbConfigState
	}

	capa := c.allCapabilities()
	var tags []string
	for k, v := range internal.GetGitMetadataTags() {
		tags = append(tags, k+":"+v)
	}
	req := clientGetConfigsRequest{
		Client: &clientData{
			State: &clientState{
				RootVersion:    uint64(state.RootsVersion),
				TargetsVersion: uint64(state.TargetsVersion),
				ConfigStates:   pbConfigState,
				HasError:       hasError,
				Error:          errMsg,
			},
			ID:       c.clientID,
			Products: c.allProducts(),
			IsTracer: true,
			ClientTracer: &clientTracer{
				RuntimeID:     c.RuntimeID,
				Language:      "go",
				TracerVersion: c.TracerVersion,
				Service:       c.ServiceName,
				Env:           c.Env,
				AppVersion:    c.AppVersion,
				ProcessTags:   processtags.GlobalTags().Slice(),
				Tags:          tags,
			},
			Capabilities: capa.Bytes(),
		},
		CachedTargetFiles: pbCachedFiles,
	}

	var b bytes.Buffer

	err = json.NewEncoder(&b).Encode(&req)
	if err != nil {
		return bytes.Buffer{}, err
	}

	return b, nil
}

var (
	idSize     = 21
	idAlphabet = []rune("_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)

func generateID() string {
	bytes := make([]byte, idSize)
	_, err := rand.Read(bytes)
	if err != nil {
		panic(err)
	}
	id := make([]rune, idSize)
	for i := range idSize {
		id[i] = idAlphabet[bytes[i]&63]
	}
	return string(id[:idSize])
}
