package producer

import (
	"context"
	"fmt"
	"io"
	"sync"

	"github.com/cockroachdb/errors"
	"go.uber.org/atomic"

	"github.com/milvus-io/milvus/internal/util/streamingutil/service/contextutil"
	"github.com/milvus-io/milvus/internal/util/streamingutil/status"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/proto/streamingpb"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/message"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/ratelimit"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/types"
	"github.com/milvus-io/milvus/pkg/v3/util/lifetime"
	"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)

// ProducerOptions is the options for creating a producer.
type ProducerOptions struct {
	// The produce target
	Assignment *types.PChannelInfoAssigned
}

// CreateProducer create a new producer client.
func CreateProducer(
	ctx context.Context,
	opts *ProducerOptions,
	handler streamingpb.StreamingNodeHandlerServiceClient,
) (Producer, error) {
	ctx = contextutil.WithCreateProducer(ctx, &streamingpb.CreateProducerRequest{
		Pchannel: types.NewProtoFromPChannelInfo(opts.Assignment.Channel),
	})
	streamClient, err := handler.Produce(ctx)
	if err != nil {
		return nil, err
	}

	// Initialize the producer client.
	produceClient := &produceGrpcClient{
		streamClient,
	}

	// Recv the first response from server.
	// It must be a create response.
	resp, err := produceClient.Recv()
	if err != nil {
		return nil, err
	}
	createResp := resp.GetCreate()
	if createResp == nil {
		return nil, status.NewInvalidRequestSeq("first message arrive must be create response")
	}

	// Initialize the producer client finished.
	cli := &producerImpl{
		RateLimitObserverRegistryImpl: ratelimit.NewRateLimitObserverRegistry(),

		assignment: *opts.Assignment,
		walName:    createResp.GetWalName(),
		logger: mlog.With(
			mlog.String("walName", createResp.GetWalName()),
			mlog.FieldPChannel(opts.Assignment.Channel.Name),
			mlog.Int64("term", opts.Assignment.Channel.Term),
			mlog.Int64("streamingNodeID", opts.Assignment.Node.ServerID)),
		lifetime:         typeutil.NewLifetime(),
		idAllocator:      typeutil.NewIDAllocator(),
		grpcStreamClient: produceClient,
		pendingRequests:  sync.Map{},
		isFenced:         atomic.NewBool(false),
		available:        lifetime.NewSafeChan(),
		requestCh:        make(chan *produceRequest),
		sendExitCh:       make(chan struct{}),
		recvExitCh:       make(chan struct{}),
		finishedCh:       make(chan struct{}),
	}

	// Start the producer client.
	go cli.execute()
	return cli, nil
}

// Expected message sequence:
// CreateProducer
// ProduceRequest 1 -> ProduceResponse Or Error 1
// ProduceRequest 2 -> ProduceResponse Or Error 2
// ProduceRequest 3 -> ProduceResponse Or Error 3
// CloseProducer
type producerImpl struct {
	*ratelimit.RateLimitObserverRegistryImpl

	assignment       types.PChannelInfoAssigned
	walName          string
	logger           *mlog.Logger
	lifetime         *typeutil.Lifetime
	idAllocator      *typeutil.IDAllocator
	grpcStreamClient *produceGrpcClient

	pendingRequests sync.Map
	isFenced        *atomic.Bool
	available       lifetime.SafeChan
	requestCh       chan *produceRequest
	sendExitCh      chan struct{}
	recvExitCh      chan struct{}
	finishedCh      chan struct{}
}

type produceRequest struct {
	ctx    context.Context
	msg    message.MutableMessage
	respCh chan produceResponse
}

type produceResponse struct {
	result *types.AppendResult
	err    error
}

// Append sends the produce message to server.
func (p *producerImpl) Append(ctx context.Context, msg message.MutableMessage) (*types.AppendResult, error) {
	if !p.lifetime.Add(typeutil.LifetimeStateWorking) {
		return nil, status.NewOnShutdownError("producer client is shutting down")
	}
	defer p.lifetime.Done()

	// reject the message if the producer is fenced.
	if p.isFenced.Load() {
		return nil, status.NewChannelFenced(p.assignment.Channel.Name)
	}
	respCh := make(chan produceResponse, 1)
	req := &produceRequest{
		ctx:    ctx,
		msg:    msg,
		respCh: respCh,
	}

	// Send the produce message to server.
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case p.requestCh <- req:
	case <-p.sendExitCh:
		return nil, status.NewInner("producer send arm is closed")
	case <-p.recvExitCh:
		return nil, status.NewInner("producer recv arm is closed")
	}

	// Wait for the response from server or context timeout.
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case resp := <-respCh:
		if resp.err != nil {
			if s := status.AsStreamingError(resp.err); s.IsFenced() || s.IsOnShutdown() {
				if p.isFenced.CompareAndSwap(false, true) {
					p.logger.Warn(ctx, "producer client is fenced or on shutdown", mlog.Err(resp.err))
					p.available.Close()
				}
			}
		}
		return resp.result, resp.err
	}
}

// execute executes the producer client.
func (p *producerImpl) execute() {
	defer close(p.finishedCh)

	errSendCh := p.startSend()
	errRecvCh := p.startRecv()

	// Wait for send and recv arm to exit.
	err := <-errRecvCh
	<-errSendCh

	// Clear all pending request.
	p.pendingRequests.Range(func(key, value interface{}) bool {
		value.(*produceRequest).respCh <- produceResponse{
			err: status.NewUnknownError(fmt.Sprintf("request sent but no response returned, %v", err)),
		}
		return true
	})
}

// IsAvailable returns whether the producer is available.
func (p *producerImpl) IsAvailable() bool {
	return !p.available.IsClosed()
}

// IsLocal returns if the producer is local.
func (p *producerImpl) IsLocal() bool {
	return false
}

// Available returns a channel that will be closed when the producer is unavailable.
func (p *producerImpl) Available() <-chan struct{} {
	return p.available.CloseCh()
}

// Close close the producer client.
func (p *producerImpl) Close() {
	// Wait for all message has been sent.
	p.lifetime.SetState(typeutil.LifetimeStateStopped)
	p.lifetime.Wait()
	close(p.requestCh)

	// Wait for send and recv arm to exit.
	<-p.finishedCh
}

// startSend starts the send loop.
func (p *producerImpl) startSend() <-chan struct{} {
	ch := make(chan struct{})
	go func() {
		_ = p.sendLoop()
		close(ch)
	}()
	return ch
}

// startRecv starts the recv loop.
func (p *producerImpl) startRecv() <-chan error {
	errCh := make(chan error, 1)
	go func() {
		errCh <- p.recvLoop()
	}()
	return errCh
}

// sendLoop sends the produce message to server.
func (p *producerImpl) sendLoop() (err error) {
	defer func() {
		if err != nil {
			p.logger.Warn(context.TODO(), "send arm of stream closed by unexpected error", mlog.Err(err))
		} else {
			p.logger.Info(context.TODO(), "send arm of stream closed")
		}
		if err := p.grpcStreamClient.CloseSend(); err != nil {
			p.logger.Warn(context.TODO(), "failed to close send", mlog.Err(err))
		}
		close(p.sendExitCh)
		p.available.Close()
	}()

	for {
		select {
		case <-p.recvExitCh:
			return status.NewOnShutdownError("recv arm of stream closed")
		case req, ok := <-p.requestCh:
			if !ok {
				// all message has been sent, sent close response.
				return p.grpcStreamClient.SendClose()
			}
			requestID := p.idAllocator.Allocate()
			// Store the request to pending request map.
			p.pendingRequests.Store(requestID, req)
			// Send the produce message to server.
			if err := p.grpcStreamClient.SendProduceMessage(req.ctx, requestID, req.msg); err != nil {
				// If send failed, remove the request from pending request map and return error to client.
				p.notifyRequest(requestID, produceResponse{
					err: err,
				})
				return err
			}
		}
	}
}

// recvLoop receives the produce response from server.
func (p *producerImpl) recvLoop() (err error) {
	defer func() {
		if err != nil {
			p.logger.Warn(context.TODO(), "recv arm of stream closed by unexpected error", mlog.Err(err))
		} else {
			p.logger.Info(context.TODO(), "recv arm of stream closed")
		}
		close(p.recvExitCh)
	}()

	for {
		resp, err := p.grpcStreamClient.Recv()
		if errors.Is(err, io.EOF) {
			return nil
		}
		if err != nil {
			return err
		}
		switch resp := resp.Response.(type) {
		case *streamingpb.ProduceResponse_RateLimit:
			p.NotifyRateLimitStateChange(ratelimit.RateLimitState{
				State: resp.RateLimit.State,
				Rate:  resp.RateLimit.Rate,
			})
		case *streamingpb.ProduceResponse_Produce:
			var result produceResponse
			switch produceResp := resp.Produce.Response.(type) {
			case *streamingpb.ProduceMessageResponse_Result:
				msgID, err := message.UnmarshalMessageID(produceResp.Result.GetId())
				if err != nil {
					return errors.Wrap(err, "failed to unmarshal message id")
				}
				lcMsgID, err := message.UnmarshalMessageID(produceResp.Result.GetLastConfirmedId())
				if err != nil {
					return errors.Wrap(err, "failed to unmarshal last confirmed message id")
				}
				result = produceResponse{
					result: &types.AppendResult{
						MessageID:              msgID,
						LastConfirmedMessageID: lcMsgID,
						TimeTick:               produceResp.Result.GetTimetick(),
						TxnCtx:                 message.NewTxnContextFromProto(produceResp.Result.GetTxnContext()),
						Extra:                  produceResp.Result.GetExtra(),
					},
				}
			case *streamingpb.ProduceMessageResponse_Error:
				statusErr := status.New(produceResp.Error.Code, produceResp.Error.Cause)
				if statusErr.IsRateLimitRejected() {
					p.NotifyRateLimitStateChange(ratelimit.RateLimitState{
						State: streamingpb.WALRateLimitState_WAL_RATE_LIMIT_STATE_REJECT,
						Rate:  0,
					})
				}
				result = produceResponse{
					err: statusErr,
				}
			default:
				// unreachable code.
				// should return error to client to promise better compatibility.
				// should never reach here.
				result = produceResponse{
					err: status.NewUnknownError(fmt.Sprintf("unknown response type: %T", resp.Produce.Response)),
				}
			}
			p.notifyRequest(resp.Produce.RequestId, result)
		case *streamingpb.ProduceResponse_Close:
			// recv io.EOF after this message.
		default:
			// skip message here.
			p.logger.Error(context.TODO(), "unknown response type", mlog.Any("response", resp))
		}
	}
}

// notifyRequest notify the request has been returned from server.
func (p *producerImpl) notifyRequest(requestID int64, resp produceResponse) {
	pendingRequest, loaded := p.pendingRequests.LoadAndDelete(requestID)
	if loaded {
		p.logger.Debug(context.TODO(), "recv send produce message from server", mlog.Int64("requestID", requestID))
		pendingRequest.(*produceRequest).respCh <- resp
	}
}
