// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 adaptor

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

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

	"github.com/milvus-io/milvus/internal/streamingnode/server/resource"
	"github.com/milvus-io/milvus/internal/streamingnode/server/wal"
	"github.com/milvus-io/milvus/internal/streamingnode/server/wal/interceptors/wab"
	"github.com/milvus-io/milvus/internal/streamingnode/server/wal/metricsutil"
	"github.com/milvus-io/milvus/internal/streamingnode/server/wal/utility"
	"github.com/milvus-io/milvus/pkg/v3/config"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/message"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/message/adaptor"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/options"
	"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/streaming/walimpls"
	"github.com/milvus-io/milvus/pkg/v3/streaming/walimpls/helper"
	"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
)

var (
	_               wal.Scanner = (*scannerAdaptorImpl)(nil)
	consumerCounter atomic.Int64
)

// newRecoveryScannerAdaptor creates a new recovery scanner adaptor.
func newRecoveryScannerAdaptor(l walimpls.ROWALImpls,
	startMessageID message.MessageID,
	scanMetrics *metricsutil.ScannerMetrics,
) *scannerAdaptorImpl {
	name := "recovery"
	logger := resource.Resource().Logger().With(
		mlog.FieldComponent("scanner"),
		mlog.String("name", name),
		mlog.String("channel", l.Channel().String()),
		mlog.String("startMessageID", startMessageID.String()),
	)
	readOption := wal.ReadOption{
		DeliverPolicy:          options.DeliverPolicyStartFrom(startMessageID),
		MesasgeHandler:         adaptor.ChanMessageHandler(make(chan message.ImmutableMessage)),
		IgnorePauseConsumption: true,
	}

	s := &scannerAdaptorImpl{
		logger:          logger,
		recovery:        true,
		innerWAL:        l,
		readOption:      readOption,
		filterFunc:      func(message.ImmutableMessage) bool { return true },
		reorderBuffer:   utility.NewReOrderBuffer(),
		pendingQueue:    utility.NewPendingQueue(),
		txnBuffer:       utility.NewTxnBuffer(logger, scanMetrics),
		cleanup:         func() {},
		ScannerHelper:   helper.NewScannerHelper(name),
		metrics:         scanMetrics,
		readRateCounter: utility.NewAverageRateCounter(10 * time.Second), // 10 second sliding window
	}
	go s.execute()
	return s
}

// newScannerAdaptor creates a new scanner adaptor.
func newScannerAdaptor(
	name string,
	l walimpls.ROWALImpls,
	readOption wal.ReadOption,
	scanMetrics *metricsutil.ScannerMetrics,
	cleanup func(),
	recovery bool,
) *scannerAdaptorImpl {
	if readOption.MesasgeHandler == nil {
		readOption.MesasgeHandler = adaptor.ChanMessageHandler(make(chan message.ImmutableMessage))
	}
	options.GetFilterFunc(readOption.MessageFilter)
	logger := resource.Resource().Logger().With(
		mlog.FieldComponent("scanner"),
		mlog.String("name", name),
		mlog.String("channel", l.Channel().Name),
	)
	s := &scannerAdaptorImpl{
		logger:          logger,
		recovery:        recovery,
		innerWAL:        l,
		readOption:      readOption,
		filterFunc:      options.GetFilterFunc(readOption.MessageFilter),
		reorderBuffer:   utility.NewReOrderBuffer(),
		pendingQueue:    utility.NewPendingQueue(),
		txnBuffer:       utility.NewTxnBuffer(logger, scanMetrics),
		cleanup:         cleanup,
		ScannerHelper:   helper.NewScannerHelper(name),
		metrics:         scanMetrics,
		readRateCounter: utility.NewAverageRateCounter(10 * time.Second), // 10 second sliding window
	}
	go s.execute()
	return s
}

// scannerAdaptorImpl is a wrapper of ScannerImpls to extend it into a Scanner interface.
type scannerAdaptorImpl struct {
	*helper.ScannerHelper
	recovery      bool
	logger        *mlog.Logger
	innerWAL      walimpls.ROWALImpls
	readOption    wal.ReadOption
	filterFunc    func(message.ImmutableMessage) bool
	reorderBuffer *utility.ReOrderByTimeTickBuffer // support time tick reorder.
	pendingQueue  *utility.PendingQueue
	txnBuffer     *utility.TxnBuffer // txn buffer for txn message.

	cleanup         func()
	clearOnce       sync.Once
	metrics         *metricsutil.ScannerMetrics
	readRateCounter *utility.AverageRateCounter // tracks read rate (bytes/sec)
}

// Channel returns the channel assignment info of the wal.
func (s *scannerAdaptorImpl) Channel() types.PChannelInfo {
	return s.innerWAL.Channel()
}

// Chan returns the message channel of the scanner.
func (s *scannerAdaptorImpl) Chan() <-chan message.ImmutableMessage {
	return s.readOption.MesasgeHandler.(adaptor.ChanMessageHandler)
}

// Close the scanner, release the underlying resources.
// Return the error same with `Error`
func (s *scannerAdaptorImpl) Close() error {
	err := s.ScannerHelper.Close()
	// Close may be called multiple times, so we need to clear the resources only once.
	s.clear()
	return err
}

// clear clears the resources of the scanner.
func (s *scannerAdaptorImpl) clear() {
	s.clearOnce.Do(func() {
		if s.cleanup != nil {
			s.cleanup()
		}
		s.metrics.Close()
	})
}

func (s *scannerAdaptorImpl) execute() {
	defer func() {
		s.readOption.MesasgeHandler.Close()
		s.Finish(nil)
		s.logger.Info(context.TODO(), "scanner is closed")
	}()
	s.logger.Info(context.TODO(), "scanner start background task")

	msgChan := make(chan message.ImmutableMessage)

	ch := make(chan struct{})
	defer func() { <-ch }()
	// TODO: optimize the extra goroutine here after msgstream is removed.
	go func() {
		defer close(ch)
		err := s.produceEventLoop(msgChan)
		if errors.Is(err, context.Canceled) {
			s.logger.Info(context.TODO(), "the produce event loop of scanner is closed")
			return
		}
		s.logger.Warn(context.TODO(), "the produce event loop of scanner is closed with unexpected error", mlog.Err(err))
	}()

	err := s.consumeEventLoop(msgChan)
	if errors.Is(err, context.Canceled) {
		s.logger.Info(context.TODO(), "the consuming event loop of scanner is closed")
		return
	}
	s.logger.Warn(context.TODO(), "the consuming event loop of scanner is closed with unexpected error", mlog.Err(err))
}

// produceEventLoop produces the message from the wal and write ahead buffer.
func (s *scannerAdaptorImpl) produceEventLoop(msgChan chan<- message.ImmutableMessage) error {
	var wb wab.ROWriteAheadBuffer
	var err error
	if s.Channel().AccessMode == types.AccessModeRW && !s.recovery {
		// recovery scanner can not use the write ahead buffer, should not trigger sync.

		// Trigger a persisted time tick to make sure the timetick is pushed forward.
		// because the underlying wal may be deleted because of retention policy.
		// So we cannot get the timetick from the wal.
		// Trigger the timetick inspector to append a new persisted timetick,
		// then the catch up scanner can see the latest timetick and make a catchup.
		resource.Resource().TimeTickInspector().TriggerSync(s.Channel(), true)
		wb = resource.Resource().TimeTickInspector().MustGetOperator(s.Channel()).WriteAheadBuffer()
	}

	scanner := newSwithableScanner(s.Name(), s.logger, s.innerWAL, wb, s.readOption.DeliverPolicy, msgChan)
	s.logger.Info(context.TODO(), "start produce loop of scanner at model", mlog.String("model", getScannerModel(scanner)))
	for {
		if s.readOption.RateLimitControl != nil {
			// if the scanner is working with rate limit control,
			// 1. when the scanner is working at catchup mode, the write operation is fast than the consume operation,
			// so we need to enter slowdown mode to protect the wal from being overloaded.
			// 2. when the scanner is working at tailing mode, the write operation is slow than the consume operation,
			// so we enter into recovery mode to speed up the rate limit.
			if _, ok := scanner.(*catchupScanner); ok {
				// Create a checker that returns false when read rate > append rate.
				// This indicates the scanner has caught up and slowdown should stop.
				checker := s.createSlowdownChecker()
				s.readOption.RateLimitControl.EnterSlowdownMode(checker)
			} else {
				s.readOption.RateLimitControl.EnterRecoveryMode()
			}
		}
		if scanner, err = scanner.Do(s.Context()); err != nil {
			return err
		}
		m := getScannerModel(scanner)
		s.metrics.SwitchModel(m)
		s.logger.Info(context.TODO(), "switch scanner model", mlog.String("model", m))
	}
}

// consumeEventLoop consumes the message from the message channel and handle it.
func (s *scannerAdaptorImpl) consumeEventLoop(msgChan <-chan message.ImmutableMessage) error {
	s.waitUntilStartConsumption()
	for {
		var upstream <-chan message.ImmutableMessage
		if s.pendingQueue.Len() > 16 {
			// If the pending queue is full, we need to wait until it's consumed to avoid scanner overloading.
			upstream = nil
		} else {
			upstream = msgChan
		}
		// generate the event channel and do the event loop.
		handleResult := s.readOption.MesasgeHandler.Handle(message.HandleParam{
			Ctx:      s.Context(),
			Upstream: upstream,
			Message:  s.pendingQueue.Next(),
		})
		if handleResult.Error != nil {
			return handleResult.Error
		}
		if handleResult.MessageHandled {
			s.pendingQueue.UnsafeAdvance()
			s.metrics.UpdatePendingQueueSize(s.pendingQueue.Bytes())
		}
		if handleResult.Incoming != nil {
			s.handleUpstream(handleResult.Incoming)
		}
	}
}

// waitUntilStartConsumption is used to wait until the consumption is started.
func (s *scannerAdaptorImpl) waitUntilStartConsumption() {
	s.metrics.PauseConsumption()
	defer s.metrics.ResumeConsumption()

	pauseConsumption := paramtable.Get().StreamingCfg.WALScannerPauseConsumption.GetAsBool()
	if !s.readOption.IgnorePauseConsumption && pauseConsumption {
		resumeChan := make(chan struct{}, 1)
		watchKey := paramtable.Get().StreamingCfg.WALScannerPauseConsumption.Key
		handler := config.NewHandler(fmt.Sprintf("%s-%d", watchKey, consumerCounter.Inc()), func(event *config.Event) {
			pause := paramtable.Get().StreamingCfg.WALScannerPauseConsumption.GetAsBool()
			if !pause {
				select {
				case resumeChan <- struct{}{}:
				default:
				}
			}
		})
		paramtable.Get().Watch(watchKey, handler)
		defer paramtable.Get().Unwatch(watchKey, handler)

		s.logger.Info(context.TODO(), "pause consumption...")
		select {
		case <-resumeChan:
			s.logger.Info(context.TODO(), "continue to consume messages")
		case <-s.Context().Done():
			s.logger.Info(context.TODO(), "pause consumption is canceled")
		}
	}
}

// createSlowdownChecker creates a SlowdownChecker for rate limit control.
// The checker returns false when read rate > append rate, indicating the scanner has caught up.
func (s *scannerAdaptorImpl) createSlowdownChecker() ratelimit.SlowdownChecker {
	appendRateCounter := s.readOption.AppendRateCounter
	if appendRateCounter == nil {
		// No append rate counter available, always continue slowdown.
		return nil
	}
	return &slowdownCheckerImpl{
		readRateCounter:   s.readRateCounter,
		appendRateCounter: appendRateCounter,
	}
}

// slowdownCheckerImpl implements ratelimit.SlowdownChecker interface.
type slowdownCheckerImpl struct {
	readRateCounter   *utility.AverageRateCounter
	appendRateCounter *utility.AverageRateCounter
}

// Check returns true if slowdown should continue, false if it should exit to recovery.
// Continue slowdown if read rate <= append rate (still catching up).
// Stop slowdown if read rate > append rate (caught up).
func (c *slowdownCheckerImpl) Check() bool {
	return c.readRateCounter.Rate() < c.appendRateCounter.Rate()*0.9
}

// SlowdownStartupHWM returns the high watermark to start slowdown from.
// Uses the current read rate as the startup HWM.
func (c *slowdownCheckerImpl) SlowdownStartupHWM() int64 {
	return int64(c.readRateCounter.Rate())
}

// handleUpstream handles the incoming message from the upstream.
func (s *scannerAdaptorImpl) handleUpstream(msg message.ImmutableMessage) {
	// Filtering the message if needed.
	// System message should never be filtered.
	if s.filterFunc != nil && !s.filterFunc(msg) {
		return
	}

	// Track read rate for rate limiting control.
	s.readRateCounter.Add(int64(msg.EstimateSize()))

	// Observe the message.
	var isTailing bool
	msg, isTailing = isTailingScanImmutableMessage(msg)
	s.metrics.ObserveMessage(isTailing, msg.MessageType(), msg.EstimateSize())
	if msg.MessageType() == message.MessageTypeTimeTick {
		// If the time tick message incoming,
		// the reorder buffer can be consumed until latest confirmed timetick.
		messages := s.reorderBuffer.PopUtilTimeTick(msg.TimeTick())
		s.metrics.UpdateTimeTickBufSize(s.reorderBuffer.Bytes())

		// There's some txn message need to hold until confirmed, so we need to handle them in txn buffer.
		msgs := s.txnBuffer.HandleImmutableMessages(messages, msg.TimeTick())
		s.metrics.UpdateTxnBufSize(s.txnBuffer.Bytes())

		if len(msgs) > 0 {
			// Push the confirmed messages into pending queue for consuming.
			if s.logger.LevelEnabled(mlog.DebugLevel) {
				for _, m := range msgs {
					s.logger.Debug(context.TODO(), "push message into pending queue",
						mlog.Uint64("committedTimeTick", msg.TimeTick()),
						mlog.FieldMessage(m),
					)
				}
			}
			s.pendingQueue.Add(msgs)
		}
		if msg.IsPersisted() || s.pendingQueue.Len() == 0 {
			// If the ts message is persisted, it must can be seen by the consumer.
			//
			// Otherwise if there's no new message incoming and there's no pending message in the queue.
			// Add current timetick message into pending queue to make timetick push forward.
			// TODO: current milvus can only run on timetick pushing,
			// after qview is applied, those trival time tick message can be erased.
			s.pendingQueue.Add([]message.ImmutableMessage{msg})
		}
		s.metrics.UpdatePendingQueueSize(s.pendingQueue.Bytes())
		return
	}

	// Filtering the vchannel
	// If the message is not belong to any vchannel, it should be broadcasted to all vchannels.
	// Otherwise, it should be filtered by vchannel.
	if msg.VChannel() != "" && s.readOption.VChannel != "" && s.readOption.VChannel != msg.VChannel() {
		return
	}
	// otherwise add message into reorder buffer directly.
	if err := s.reorderBuffer.Push(msg); err != nil {
		if errors.Is(err, utility.ErrTimeTickVoilation) {
			s.metrics.ObserveTimeTickViolation(isTailing, msg.MessageType())
		}
		s.logger.Warn(context.TODO(), "failed to push message into reorder buffer",
			mlog.FieldMessage(msg),
			mlog.Bool("tailing", isTailing),
			mlog.Err(err))
	}
	// Observe the filtered message.
	s.metrics.UpdateTimeTickBufSize(s.reorderBuffer.Bytes())
	s.metrics.ObservePassedMessage(isTailing, msg.MessageType(), msg.EstimateSize())
}
