package assignment

import (
	"io"
	"sync"

	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"

	"github.com/milvus-io/milvus/internal/util/streamingutil/status"
	"github.com/milvus-io/milvus/pkg/v2/log"
	"github.com/milvus-io/milvus/pkg/v2/proto/streamingpb"
	"github.com/milvus-io/milvus/pkg/v2/streaming/util/types"
	"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
	"github.com/milvus-io/milvus/pkg/v2/util/replicateutil"
	"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)

// newAssignmentDiscoverClient creates a new assignment discover client.
func newAssignmentDiscoverClient(w *watcher, streamClient streamingpb.StreamingCoordAssignmentService_AssignmentDiscoverClient) *assignmentDiscoverClient {
	c := &assignmentDiscoverClient{
		lifetime:              typeutil.NewLifetime(),
		w:                     w,
		streamClient:          streamClient,
		logger:                log.With(),
		requestCh:             make(chan *streamingpb.AssignmentDiscoverRequest, 16),
		exitCh:                make(chan struct{}),
		wg:                    sync.WaitGroup{},
		lastErrorReportedTerm: make(map[string]int64),
		clusterID:             paramtable.Get().CommonCfg.ClusterPrefix.GetValue(),
	}
	c.executeBackgroundTask()
	return c
}

// assignmentDiscoverClient is the client for assignment discover.
type assignmentDiscoverClient struct {
	lifetime              *typeutil.Lifetime
	w                     *watcher
	logger                *log.MLogger
	requestCh             chan *streamingpb.AssignmentDiscoverRequest
	exitCh                chan struct{}
	wg                    sync.WaitGroup
	streamClient          streamingpb.StreamingCoordAssignmentService_AssignmentDiscoverClient
	lastErrorReportedTerm map[string]int64
	clusterID             string
}

// ReportAssignmentError reports the assignment error to server.
func (c *assignmentDiscoverClient) ReportAssignmentError(pchannel types.PChannelInfo, err error) {
	if !c.lifetime.Add(typeutil.LifetimeStateWorking) {
		return
	}
	defer c.lifetime.Done()

	statusErr := status.AsStreamingError(err).AsPBError()
	select {
	case c.requestCh <- &streamingpb.AssignmentDiscoverRequest{
		Command: &streamingpb.AssignmentDiscoverRequest_ReportError{
			ReportError: &streamingpb.ReportAssignmentErrorRequest{
				Pchannel: types.NewProtoFromPChannelInfo(pchannel),
				Err:      statusErr,
			},
		},
	}:
	case <-c.exitCh:
	}
}

func (c *assignmentDiscoverClient) IsAvailable() bool {
	select {
	case <-c.Available():
		return false
	default:
		return true
	}
}

// Available returns a channel that will be closed when the assignment discover client is available.
func (c *assignmentDiscoverClient) Available() <-chan struct{} {
	return c.exitCh
}

// Close closes the assignment discover client.
func (c *assignmentDiscoverClient) Close() {
	c.lifetime.SetState(typeutil.LifetimeStateStopped)
	c.lifetime.Wait()

	close(c.requestCh)
	c.wg.Wait()
}

func (c *assignmentDiscoverClient) executeBackgroundTask() {
	c.wg.Add(2)
	go c.recvLoop()
	go c.sendLoop()
}

// sendLoop sends the request to server.
func (c *assignmentDiscoverClient) sendLoop() (err error) {
	defer c.wg.Done()
	for {
		req, ok := <-c.requestCh
		if !ok {
			// send close message and close send operation.
			if err := c.streamClient.Send(&streamingpb.AssignmentDiscoverRequest{
				Command: &streamingpb.AssignmentDiscoverRequest_Close{},
			}); err != nil {
				return err
			}
			return c.streamClient.CloseSend()
		}
		if c.shouldIgnore(req) {
			continue
		}
		if err := c.streamClient.Send(req); err != nil {
			return err
		}
	}
}

// shouldIgnore checks if the request should be ignored.
func (c *assignmentDiscoverClient) shouldIgnore(req *streamingpb.AssignmentDiscoverRequest) bool {
	switch req := req.Command.(type) {
	case *streamingpb.AssignmentDiscoverRequest_ReportError:
		if term, ok := c.lastErrorReportedTerm[req.ReportError.Pchannel.Name]; ok && req.ReportError.Pchannel.Term <= term {
			// If the error at newer term has been reported, ignore it right now.
			return true
		}
		c.lastErrorReportedTerm[req.ReportError.Pchannel.Name] = req.ReportError.Pchannel.Term
	}
	return false
}

// recvLoop receives the message from server.
// 1. FullAssignment
// 2. Close
func (c *assignmentDiscoverClient) recvLoop() (err error) {
	defer func() {
		c.wg.Done()
		close(c.exitCh)
	}()
	for {
		resp, err := c.streamClient.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		switch resp := resp.Response.(type) {
		case *streamingpb.AssignmentDiscoverResponse_FullAssignment:
			if resp.FullAssignment.VersionByRevision == nil {
				marshaledFullAssignment, _ := protojson.Marshal(resp.FullAssignment)
				c.logger.Warn("VersionByRevision is nil, from legacy mixcoord server, skipping", zap.String("assignment", string(marshaledFullAssignment)))
				continue
			}
			newIncomingVersion := typeutil.VersionInt64Pair{
				// Version field is wrong implementation, so using VersionByRevision instead here.
				Global: resp.FullAssignment.VersionByRevision.Global,
				Local:  resp.FullAssignment.VersionByRevision.Local,
			}
			newIncomingAssignments := make(map[int64]types.StreamingNodeAssignment, len(resp.FullAssignment.Assignments))
			for _, assignment := range resp.FullAssignment.Assignments {
				channels := make(map[string]types.PChannelInfo, len(assignment.Channels))
				for _, channel := range assignment.Channels {
					channels[channel.Name] = types.NewPChannelInfoFromProto(channel)
				}
				newIncomingAssignments[assignment.GetNode().GetServerId()] = types.StreamingNodeAssignment{
					NodeInfo: types.NewStreamingNodeInfoFromProto(assignment.Node),
					Channels: channels,
				}
			}
			c.w.Update(types.VersionedStreamingNodeAssignments{
				StreamingVersion: resp.FullAssignment.StreamingVersion,
				Version:          newIncomingVersion,
				Assignments:      newIncomingAssignments,
				CChannel:         resp.FullAssignment.Cchannel,
				ReplicateConfigHelper: replicateutil.MustNewConfigHelper(
					c.clusterID,
					resp.FullAssignment.ReplicateConfiguration),
			})
		case *streamingpb.AssignmentDiscoverResponse_Close:
			// nothing to do now, just wait io.EOF.
		}
	}
}
