package writebuffer

import (
	"context"
	"sync"
	"time"

	"golang.org/x/time/rate"

	"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
	"github.com/milvus-io/milvus/internal/flushcommon/metacache"
	"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/mq/msgstream"
	"github.com/milvus-io/milvus/pkg/v3/util/hardware"
	"github.com/milvus-io/milvus/pkg/v3/util/lifetime"
	"github.com/milvus-io/milvus/pkg/v3/util/logutil"
	"github.com/milvus-io/milvus/pkg/v3/util/merr"
	"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
	"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)

// BufferManager is the interface for WriteBuffer management.
//
//go:generate mockery --name=BufferManager --structname=MockBufferManager --output=./  --filename=mock_manager.go --with-expecter --inpackage
type BufferManager interface {
	// Register adds a WriteBuffer with provided schema & options.
	Register(channel string, metacache metacache.MetaCache, opts ...WriteBufferOption) error
	// CreateNewGrowingSegment notifies writeBuffer to create a new growing segment.
	CreateNewGrowingSegment(ctx context.Context, channel string, info CreateGrowingSegmentInfo) error
	// SealSegments notifies writeBuffer corresponding to provided channel to seal segments.
	// which will cause segment start flush procedure.
	SealSegments(ctx context.Context, channel string, segmentIDs []int64) error
	// SealAllSegments notifies writeBuffer to seal all segments.
	SealAllSegments(ctx context.Context, channel string) error
	// FlushChannel set the flushTs of the provided write buffer.
	FlushChannel(ctx context.Context, channel string, flushTs uint64) error
	// RemoveChannel removes a write buffer from manager.
	RemoveChannel(channel string)
	// DropChannel remove write buffer and perform drop.
	DropChannel(channel string)
	DropPartitions(channel string, partitionIDs []int64)
	// BufferData put data into channel write buffer.
	BufferData(channel string, insertData []*InsertData, deleteMsgs []*msgstream.DeleteMsg, startPos, endPos *msgpb.MsgPosition, schemaVersion int32) error
	// GetCheckpoint returns checkpoint for provided channel.
	GetCheckpoint(channel string) (*msgpb.MsgPosition, bool, error)
	// NotifyCheckpointUpdated notify write buffer checkpoint updated to reset flushTs.
	NotifyCheckpointUpdated(channel string, ts uint64)

	// AllowGrowingSourceFlush returns true if this channel may try growing-source flush.
	AllowGrowingSourceFlush(channel string) bool
	// GetGrowingFlushProgress returns growing-source progress for the given channel.
	// If segmentIDs is empty, all tracked growing-source segments are returned.
	// Otherwise, the requested segmentIDs are returned together with all tracked
	// growing-source segments so release handoff cannot miss existing source progress.
	GetGrowingFlushProgress(ctx context.Context, channel string, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error)

	// Start makes the background check start to work.
	Start()
	// Stop the background checker and wait for worker goroutine quit.
	Stop()
}

type ReleaseManualFlushNeedChecker interface {
	CheckReleaseManualFlushNeed(ctx context.Context, channel string, segmentIDs []int64) (bool, error)
}

// NewManager returns initialized manager as `Manager`
func NewManager(syncMgr syncmgr.SyncManager) BufferManager {
	return &bufferManager{
		syncMgr: syncMgr,
		buffers: typeutil.NewConcurrentMap[string, WriteBuffer](),

		ch: lifetime.NewSafeChan(),
	}
}

type bufferManager struct {
	syncMgr syncmgr.SyncManager
	buffers *typeutil.ConcurrentMap[string, WriteBuffer]

	wg sync.WaitGroup
	ch lifetime.SafeChan
}

func (m *bufferManager) Start() {
	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		m.check()
	}()
}

func (m *bufferManager) check() {
	timer := time.NewTimer(paramtable.Get().DataNodeCfg.MemoryCheckInterval.GetAsDuration(time.Millisecond))
	defer timer.Stop()
	for {
		select {
		case <-timer.C:
			m.memoryCheck()
			if !timer.Stop() {
				select {
				case <-timer.C:
				default:
				}
			}
			timer.Reset(paramtable.Get().DataNodeCfg.MemoryCheckInterval.GetAsDuration(time.Millisecond))
		case <-m.ch.CloseCh():
			mlog.Info(context.TODO(), "buffer manager memory check stopped")
			return
		}
	}
}

// memoryCheck performs check based on current memory usage & configuration.
func (m *bufferManager) memoryCheck() {
	if !paramtable.Get().DataNodeCfg.MemoryForceSyncEnable.GetAsBool() {
		return
	}
	startTime := time.Now()
	defer func() {
		dur := time.Since(startTime)
		if dur > 30*time.Second {
			mlog.Warn(context.TODO(), "memory check takes too long", mlog.Duration("time", dur))
		}
	}()

	for {
		var total int64
		var candidate WriteBuffer
		var candiSize int64
		var candiChan string

		select {
		case <-m.ch.CloseCh():
			mlog.Info(context.TODO(), "stop memory check due to manager stop")
			return
		default:
		}

		m.buffers.Range(func(chanName string, buf WriteBuffer) bool {
			size := buf.MemorySize()
			total += size
			if size > candiSize {
				candiSize = size
				candidate = buf
				candiChan = chanName
			}
			return true
		})

		totalMemory := hardware.GetMemoryCount()
		memoryWatermark := float64(totalMemory) * paramtable.Get().DataNodeCfg.MemoryForceSyncWatermark.GetAsFloat()
		if float64(total) < memoryWatermark {
			mlog.RatedDebug(context.TODO(), rate.Limit(20), "skip force sync because memory level is not high enough",
				mlog.Float64("current_total_memory_usage", logutil.ToMB(float64(total))),
				mlog.Float64("current_memory_watermark", logutil.ToMB(memoryWatermark)))
			return
		}

		if candidate != nil {
			candidate.EvictBuffer(GetOldestBufferPolicy(paramtable.Get().DataNodeCfg.MemoryForceSyncSegmentNum.GetAsInt()))
			mlog.Info(context.TODO(), "notify writebuffer to sync",
				mlog.String("channel", candiChan), mlog.Float64("bufferSize(MB)", logutil.ToMB(float64(candiSize))))
		}
	}
}

func (m *bufferManager) Stop() {
	m.ch.Close()
	m.wg.Wait()
}

// Register a new WriteBuffer for channel.
func (m *bufferManager) Register(channel string, metacache metacache.MetaCache, opts ...WriteBufferOption) error {
	buf, err := NewWriteBuffer(channel, metacache, m.syncMgr, opts...)
	if err != nil {
		return err
	}

	_, loaded := m.buffers.GetOrInsert(channel, buf)
	if loaded {
		buf.Close(context.Background(), false)
		return merr.WrapErrChannelReduplicate(channel)
	}
	return nil
}

// CreateNewGrowingSegment notifies writeBuffer to create a new growing segment.
func (m *bufferManager) CreateNewGrowingSegment(ctx context.Context, channel string, info CreateGrowingSegmentInfo) error {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(ctx, "write buffer not found when create new growing segment",
			mlog.String("channel", channel),
			mlog.FieldPartitionID(info.PartitionID),
			mlog.FieldSegmentID(info.SegmentID))
		return merr.WrapErrChannelNotFound(channel)
	}
	return buf.CreateNewGrowingSegment(info)
}

// SealSegments call sync segment and change segments state to Flushed.
func (m *bufferManager) SealSegments(ctx context.Context, channel string, segmentIDs []int64) error {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(ctx, "write buffer not found when flush segments",
			mlog.String("channel", channel),
			mlog.Int64s("segmentIDs", segmentIDs))
		return merr.WrapErrChannelNotFound(channel)
	}

	return buf.SealSegments(ctx, segmentIDs)
}

// SealAllSegments seal all segments in the write buffer.
func (m *bufferManager) SealAllSegments(ctx context.Context, channel string) error {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(ctx, "write buffer not found",
			mlog.String("channel", channel))
		return merr.WrapErrChannelNotFound(channel)
	}

	buf.SealAllSegments(ctx)
	return nil
}

func (m *bufferManager) FlushChannel(ctx context.Context, channel string, flushTs uint64) error {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(ctx, "write buffer not found when flush channel",
			mlog.String("channel", channel),
			mlog.Uint64("flushTs", flushTs))
		return merr.WrapErrChannelNotFound(channel)
	}
	buf.SetFlushTimestamp(flushTs)
	return nil
}

// BufferData put data into channel write buffer.
func (m *bufferManager) BufferData(channel string, insertData []*InsertData, deleteMsgs []*msgstream.DeleteMsg, startPos, endPos *msgpb.MsgPosition, schemaVersion int32) error {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(context.TODO(), "write buffer not found when buffer data",
			mlog.String("channel", channel))
		return merr.WrapErrChannelNotFound(channel)
	}

	return buf.BufferData(insertData, deleteMsgs, startPos, endPos, schemaVersion)
}

func (m *bufferManager) AllowGrowingSourceFlush(channel string) bool {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		return false
	}
	return buf.AllowGrowingSourceFlush()
}

func (m *bufferManager) CheckReleaseManualFlushNeed(ctx context.Context, channel string, segmentIDs []int64) (bool, error) {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(ctx, "write buffer not found when checking release manual flush",
			mlog.String("channel", channel),
			mlog.Int64s("segmentIDs", segmentIDs))
		return true, merr.WrapErrChannelNotFound(channel)
	}
	checker, ok := buf.(interface {
		CheckReleaseManualFlushNeed(segmentIDs []int64) bool
	})
	if !ok {
		return true, nil
	}
	return checker.CheckReleaseManualFlushNeed(segmentIDs), nil
}

func (m *bufferManager) GetGrowingFlushProgress(ctx context.Context, channel string, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error) {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(ctx, "write buffer not found when get growing flush progress",
			mlog.String("channel", channel),
			mlog.Int64s("segmentIDs", segmentIDs),
			mlog.Uint64("fenceTs", fenceTs))
		return nil, merr.WrapErrChannelNotFound(channel)
	}
	return buf.GetGrowingFlushProgress(ctx, segmentIDs, fenceTs)
}

// GetCheckpoint returns checkpoint for provided channel.
func (m *bufferManager) GetCheckpoint(channel string) (*msgpb.MsgPosition, bool, error) {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		return nil, false, merr.WrapErrChannelNotFound(channel)
	}
	cp := buf.GetCheckpoint()
	flushTs := buf.GetFlushTimestamp()

	return cp, flushTs != nonFlushTS && cp.GetTimestamp() >= flushTs, nil
}

func (m *bufferManager) NotifyCheckpointUpdated(channel string, ts uint64) {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		return
	}
	flushTs := buf.GetFlushTimestamp()
	if flushTs != nonFlushTS && ts > flushTs {
		mlog.Info(context.TODO(), "reset channel flushTs", mlog.String("channel", channel))
		buf.SetFlushTimestamp(nonFlushTS)
	}
}

// RemoveChannel remove channel WriteBuffer from manager.
// this method discards all buffered data since datanode no longer has the ownership
func (m *bufferManager) RemoveChannel(channel string) {
	buf, loaded := m.buffers.GetAndRemove(channel)
	if !loaded {
		mlog.Warn(context.TODO(), "failed to remove channel, channel not maintained in manager", mlog.String("channel", channel))
		return
	}

	buf.Close(context.Background(), false)
}

// DropChannel removes channel WriteBuffer and process `DropChannel`
// this method will save all buffered data
func (m *bufferManager) DropChannel(channel string) {
	buf, loaded := m.buffers.GetAndRemove(channel)
	if !loaded {
		mlog.Warn(context.TODO(), "failed to drop channel, channel not maintained in manager", mlog.String("channel", channel))
		return
	}

	buf.Close(context.Background(), true)
}

func (m *bufferManager) DropPartitions(channel string, partitionIDs []int64) {
	buf, loaded := m.buffers.Get(channel)
	if !loaded {
		mlog.Warn(context.TODO(), "failed to drop partition, channel not maintained in manager", mlog.String("channel", channel), mlog.Int64s("partitionIDs", partitionIDs))
		return
	}

	buf.DropPartitions(partitionIDs)
}
