/*
 *
 * Copyright 2025 gRPC 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 lrsclient provides an LRS (Load Reporting Service) client.
//
// See: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/load_stats/v3/lrs.proto
package lrsclient

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	igrpclog "google.golang.org/grpc/internal/grpclog"
	"google.golang.org/grpc/internal/xds/clients"
	clientsinternal "google.golang.org/grpc/internal/xds/clients/internal"
	"google.golang.org/grpc/internal/xds/clients/internal/backoff"
)

const (
	clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning"
	clientFeatureResourceWrapper    = "xds.config.resource-in-sotw"
)

var (
	defaultExponentialBackoff = backoff.DefaultExponential.Backoff
)

// LRSClient is an LRS (Load Reporting Service) client.
type LRSClient struct {
	transportBuilder clients.TransportBuilder
	node             clients.Node
	backoff          func(int) time.Duration // Backoff for LRS stream failures.
	logger           *igrpclog.PrefixLogger

	// The LRSClient owns a bunch of streams to individual LRS servers.
	//
	// Once all references to a stream are dropped, the stream is closed.
	mu         sync.Mutex
	lrsStreams map[clients.ServerIdentifier]*streamImpl // Map from server config to in-use streamImpls.
	lrsRefs    map[clients.ServerIdentifier]int         // Map from server config to number of references.
}

// New returns a new LRS Client configured with the provided config.
func New(config Config) (*LRSClient, error) {
	if config.TransportBuilder == nil {
		return nil, errors.New("lrsclient: transport builder is nil")
	}

	c := &LRSClient{
		transportBuilder: config.TransportBuilder,
		node:             config.Node,
		backoff:          defaultExponentialBackoff,
		lrsStreams:       make(map[clients.ServerIdentifier]*streamImpl),
		lrsRefs:          make(map[clients.ServerIdentifier]int),
	}
	c.logger = prefixLogger(c)
	return c, nil
}

// ReportLoad creates and returns a LoadStore for the caller to report loads
// using a LoadReportingStream.
//
// Caller must call Stop on the returned LoadStore when they are done reporting
// load to this server.
func (c *LRSClient) ReportLoad(si clients.ServerIdentifier) (*LoadStore, error) {
	lrs, err := c.getOrCreateLRSStream(si)
	if err != nil {
		return nil, err
	}
	return lrs.loadStore, nil
}

// getOrCreateLRSStream returns an lrs stream for the given server identifier.
//
// If an active lrs stream exists for the given server identifier, it is
// returned. Otherwise, a new lrs stream is created and returned.
func (c *LRSClient) getOrCreateLRSStream(serverIdentifier clients.ServerIdentifier) (*streamImpl, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if c.logger.V(2) {
		c.logger.Infof("Received request for a reference to an lrs stream for server identifier %q", serverIdentifier)
	}

	// Use an existing stream, if one exists for this server identifier.
	if s, ok := c.lrsStreams[serverIdentifier]; ok {
		if c.logger.V(2) {
			c.logger.Infof("Reusing an existing lrs stream for server identifier %q", serverIdentifier)
		}
		c.lrsRefs[serverIdentifier]++
		return s, nil
	}

	if c.logger.V(2) {
		c.logger.Infof("Creating a new lrs stream for server identifier %q", serverIdentifier)
	}

	// Create a new transport and create a new lrs stream, and add it to the
	// map of lrs streams.
	tr, err := c.transportBuilder.Build(serverIdentifier)
	if err != nil {
		return nil, fmt.Errorf("lrsclient: failed to create transport for server identifier %s: %v", serverIdentifier, err)
	}

	nodeProto := clientsinternal.NodeProto(c.node)
	nodeProto.ClientFeatures = []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}
	lrs := newStreamImpl(streamOpts{
		transport: tr,
		backoff:   c.backoff,
		nodeProto: nodeProto,
		logPrefix: clientPrefix(c),
	})

	// Register a stop function that decrements the reference count, stops
	// the LRS stream when the last reference is removed and closes the
	// transport and removes the lrs stream and its references from the
	// respective maps. Before closing the stream, it waits for the provided
	// context to be done (timeout or cancellation).
	stop := func(ctx context.Context) {
		c.mu.Lock()
		defer c.mu.Unlock()

		if r, ok := c.lrsRefs[serverIdentifier]; !ok || r == 0 {
			c.logger.Errorf("Attempting to stop already stopped StreamImpl")
			return
		}
		c.lrsRefs[serverIdentifier]--
		if c.lrsRefs[serverIdentifier] != 0 {
			return
		}

		lrs.finalSendRequest <- struct{}{}

		select {
		case err := <-lrs.finalSendDone:
			if err != nil {
				c.logger.Warningf("Final send attempt failed: %v", err)
			}
		case <-ctx.Done():
			c.logger.Warningf("Context canceled before finishing the final send attempt: %v", err)
		}

		lrs.cancelStream()
		lrs.cancelStream = nil
		lrs.logger.Infof("Stopping LRS stream")
		<-lrs.doneCh

		delete(c.lrsStreams, serverIdentifier)
		tr.Close()
	}
	lrs.loadStore.stop = stop

	c.lrsStreams[serverIdentifier] = lrs
	c.lrsRefs[serverIdentifier] = 1

	return lrs, nil
}
