// 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 segments

/*
#cgo pkg-config: milvus_core

#include "futures/future_c.h"
#include "segcore/collection_c.h"
#include "segcore/plan_c.h"
#include "segcore/segment_c.h"
#include "common/init_c.h"
*/
import "C"

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"
	"unsafe"

	"github.com/cockroachdb/errors"
	"go.opentelemetry.io/otel"
	"go.uber.org/atomic"
	"google.golang.org/protobuf/proto"

	"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
	"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
	"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
	"github.com/milvus-io/milvus/internal/querynodev2/pkoracle"
	"github.com/milvus-io/milvus/internal/querynodev2/segments/state"
	"github.com/milvus-io/milvus/internal/storage"
	"github.com/milvus-io/milvus/internal/storagev2/packed"
	"github.com/milvus-io/milvus/internal/util/indexparamcheck"
	"github.com/milvus-io/milvus/internal/util/segcore"
	"github.com/milvus-io/milvus/internal/util/vecindexmgr"
	"github.com/milvus-io/milvus/pkg/v3/common"
	"github.com/milvus-io/milvus/pkg/v3/metrics"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/proto/cgopb"
	"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
	"github.com/milvus-io/milvus/pkg/v3/proto/querypb"
	"github.com/milvus-io/milvus/pkg/v3/proto/segcorepb"
	"github.com/milvus-io/milvus/pkg/v3/util/contextutil"
	"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
	"github.com/milvus-io/milvus/pkg/v3/util/indexparams"
	"github.com/milvus-io/milvus/pkg/v3/util/merr"
	"github.com/milvus-io/milvus/pkg/v3/util/metautil"
	"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
	"github.com/milvus-io/milvus/pkg/v3/util/timerecord"
	"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)

type SegmentType = commonpb.SegmentState

const (
	SegmentTypeGrowing = commonpb.SegmentState_Growing
	SegmentTypeSealed  = commonpb.SegmentState_Sealed
)

var ErrSegmentUnhealthy = errors.New("segment unhealthy")

// IndexedFieldInfo contains binlog info of vector field
type IndexedFieldInfo struct {
	FieldBinlog *datapb.FieldBinlog
	IndexInfo   *querypb.FieldIndexInfo
	IsLoaded    bool
}

type baseSegment struct {
	collection *Collection
	version    *atomic.Int64

	segmentType   SegmentType
	pkCandidate   pkoracle.Candidate // PK candidate: BloomFilterSet for regular collections, ExternalSegmentCandidate for external collections
	loadInfo      *atomic.Pointer[querypb.SegmentLoadInfo]
	skipGrowingBF bool // Skip generating or maintaining BF for growing segments; deletion checks will be handled in segcore.
	channel       metautil.Channel

	resourceUsageCache *atomic.Pointer[ResourceUsage]

	needUpdatedVersion *atomic.Int64 // only for lazy load mode update index
}

func newBaseSegment(collection *Collection, segmentType SegmentType, version int64, loadInfo *querypb.SegmentLoadInfo) (baseSegment, error) {
	channel, err := metautil.ParseChannel(loadInfo.GetInsertChannel(), channelMapper)
	if err != nil {
		return baseSegment{}, err
	}
	bs := baseSegment{
		collection:    collection,
		loadInfo:      atomic.NewPointer[querypb.SegmentLoadInfo](loadInfo),
		version:       atomic.NewInt64(version),
		segmentType:   segmentType,
		pkCandidate:   pkoracle.NewBloomFilterSet(loadInfo.GetSegmentID(), loadInfo.GetPartitionID(), segmentType),
		channel:       channel,
		skipGrowingBF: segmentType == SegmentTypeGrowing && paramtable.Get().QueryNodeCfg.SkipGrowingSegmentBF.GetAsBool(),

		resourceUsageCache: atomic.NewPointer[ResourceUsage](nil),
		needUpdatedVersion: atomic.NewInt64(0),
	}
	return bs, nil
}

// ID returns the identity number.
func (s *baseSegment) ID() int64 {
	return s.loadInfo.Load().GetSegmentID()
}

func (s *baseSegment) Collection() int64 {
	return s.loadInfo.Load().GetCollectionID()
}

func (s *baseSegment) GetCollection() *Collection {
	return s.collection
}

func (s *baseSegment) Partition() int64 {
	return s.loadInfo.Load().GetPartitionID()
}

func (s *baseSegment) DatabaseName() string {
	return s.collection.GetDBName()
}

func (s *baseSegment) ResourceGroup() string {
	return s.collection.GetResourceGroup()
}

func (s *baseSegment) Shard() metautil.Channel {
	return s.channel
}

func (s *baseSegment) Type() SegmentType {
	return s.segmentType
}

func (s *baseSegment) Level() datapb.SegmentLevel {
	return s.loadInfo.Load().GetLevel()
}

func (s *baseSegment) IsSorted() bool {
	return s.loadInfo.Load().GetIsSorted()
}

func (s *baseSegment) StartPosition() *msgpb.MsgPosition {
	return s.loadInfo.Load().GetStartPosition()
}

func (s *baseSegment) Version() int64 {
	return s.version.Load()
}

func (s *baseSegment) CASVersion(old, newVersion int64) bool {
	return s.version.CompareAndSwap(old, newVersion)
}

func (s *baseSegment) LoadInfo() *querypb.SegmentLoadInfo {
	return s.loadInfo.Load()
}

func cloneMsgPosition(position *msgpb.MsgPosition) *msgpb.MsgPosition {
	if position == nil {
		return nil
	}
	return proto.Clone(position).(*msgpb.MsgPosition)
}

// compactSegmentLoadInfoForRuntime retains runtime identity/version metadata,
// positions, deltalogs, and manifest/resource estimate fields; callers must
// consume binlog, index, and stats metadata before compaction.
func compactSegmentLoadInfoForRuntime(loadInfo *querypb.SegmentLoadInfo) *querypb.SegmentLoadInfo {
	if loadInfo == nil {
		return nil
	}
	return &querypb.SegmentLoadInfo{
		SegmentID:            loadInfo.GetSegmentID(),
		PartitionID:          loadInfo.GetPartitionID(),
		CollectionID:         loadInfo.GetCollectionID(),
		DbID:                 loadInfo.GetDbID(),
		FlushTime:            loadInfo.GetFlushTime(),
		NumOfRows:            loadInfo.GetNumOfRows(),
		Deltalogs:            loadInfo.GetDeltalogs(),
		CompactionFrom:       append([]int64(nil), loadInfo.GetCompactionFrom()...),
		SegmentSize:          loadInfo.GetSegmentSize(),
		InsertChannel:        loadInfo.GetInsertChannel(),
		StartPosition:        cloneMsgPosition(loadInfo.GetStartPosition()),
		DeltaPosition:        cloneMsgPosition(loadInfo.GetDeltaPosition()),
		ReadableVersion:      loadInfo.GetReadableVersion(),
		Level:                loadInfo.GetLevel(),
		StorageVersion:       loadInfo.GetStorageVersion(),
		IsSorted:             loadInfo.GetIsSorted(),
		Priority:             loadInfo.GetPriority(),
		ManifestPath:         loadInfo.GetManifestPath(),
		DataVersion:          loadInfo.GetDataVersion(),
		UseTakeForOutput:     loadInfo.GetUseTakeForOutput(),
		CommitTimestamp:      loadInfo.GetCommitTimestamp(),
		EstimatedBytesPerRow: loadInfo.GetEstimatedBytesPerRow(),
	}
}

func (s *baseSegment) compactLoadInfoForRuntime() {
	if s.segmentType != SegmentTypeSealed || s.Level() == datapb.SegmentLevel_L0 {
		return
	}
	loadInfo := s.LoadInfo()
	usage, err := estimateLogicalResourceUsageOfSegment(s.collection.Schema(), loadInfo, resourceEstimateFactor{
		deltaDataExpansionFactor:        paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.GetAsFloat(),
		TieredEvictionEnabled:           paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.GetAsBool(),
		TieredEvictableMemoryCacheRatio: paramtable.Get().QueryNodeCfg.TieredEvictableMemoryCacheRatio.GetAsFloat(),
		TieredEvictableDiskCacheRatio:   paramtable.Get().QueryNodeCfg.TieredEvictableDiskCacheRatio.GetAsFloat(),
	})
	if err != nil {
		s.resourceUsageCache.Store(nil)
		mlog.Warn(context.TODO(), "unreachable: failed to get resource usage estimate of segment", mlog.Err(err), mlog.FieldCollectionID(s.Collection()), mlog.FieldSegmentID(s.ID()))
		return
	}
	s.resourceUsageCache.Store(usage)
	s.loadInfo.Store(compactSegmentLoadInfoForRuntime(loadInfo))
}

func (s *baseSegment) SetPKCandidate(candidate pkoracle.Candidate) {
	s.pkCandidate = candidate
}

// PkCandidateExist implements pkoracle.Candidate — reports whether PK data has been loaded.
func (s *baseSegment) PkCandidateExist() bool {
	return s.pkCandidate != nil && s.pkCandidate.PkCandidateExist()
}

// UpdatePkCandidate feeds new primary keys into the PK candidate.
func (s *baseSegment) UpdatePkCandidate(pks []storage.PrimaryKey) {
	if s.skipGrowingBF {
		return
	}
	if s.pkCandidate != nil {
		s.pkCandidate.UpdatePkCandidate(pks)
	}
}

// Stats implements pkoracle.Candidate — returns PK statistics (min/max PK).
func (s *baseSegment) Stats() *storage.PkStatistics {
	if s.pkCandidate != nil {
		return s.pkCandidate.Stats()
	}
	return nil
}

// Charge implements pkoracle.Candidate — charges memory resources.
func (s *baseSegment) Charge() {
	if s.pkCandidate != nil {
		s.pkCandidate.Charge()
	}
}

// Refund implements pkoracle.Candidate — releases memory resources.
func (s *baseSegment) Refund() {
	if s.pkCandidate != nil {
		s.pkCandidate.Refund()
	}
}

type bm25StatsHolder struct {
	mu    sync.RWMutex
	stats map[int64]*storage.BM25Stats
}

func newBM25StatsHolder() *bm25StatsHolder {
	return &bm25StatsHolder{
		stats: make(map[int64]*storage.BM25Stats),
	}
}

func (h *bm25StatsHolder) UpdateBM25Stats(stats map[int64]*storage.BM25Stats) {
	if h == nil {
		return
	}

	h.mu.Lock()
	defer h.mu.Unlock()

	for fieldID, new := range stats {
		if new == nil {
			continue
		}
		if current, ok := h.stats[fieldID]; ok {
			current.Merge(new)
		} else {
			h.stats[fieldID] = new.Clone()
		}
	}
}

func (h *bm25StatsHolder) GetBM25Stats() map[int64]*storage.BM25Stats {
	if h == nil {
		return map[int64]*storage.BM25Stats{}
	}

	h.mu.RLock()
	defer h.mu.RUnlock()

	stats := cloneBM25StatsMap(h.stats)
	if stats == nil {
		return map[int64]*storage.BM25Stats{}
	}
	return stats
}

func cloneBM25StatsMap(stats map[int64]*storage.BM25Stats) map[int64]*storage.BM25Stats {
	if len(stats) == 0 {
		return nil
	}
	cloned := make(map[int64]*storage.BM25Stats, len(stats))
	for fieldID, stat := range stats {
		if stat != nil {
			cloned[fieldID] = stat.Clone()
		}
	}
	return cloned
}

// MayPkExist returns true if the given PK exists in the PK range and being positive through the bloom filter,
// false otherwise,
// may returns true even the PK doesn't exist actually
func (s *baseSegment) MayPkExist(pk *storage.LocationsCache) bool {
	if s.skipGrowingBF {
		return true
	}
	if s.pkCandidate == nil {
		return true // No candidate, assume PK might exist
	}
	return s.pkCandidate.MayPkExist(pk)
}

func (s *baseSegment) GetMinPk() *storage.PrimaryKey {
	if s.pkCandidate != nil {
		if stats := s.pkCandidate.Stats(); stats != nil {
			return &stats.MinPK
		}
	}
	return nil
}

func (s *baseSegment) GetMaxPk() *storage.PrimaryKey {
	if s.pkCandidate != nil {
		if stats := s.pkCandidate.Stats(); stats != nil {
			return &stats.MaxPK
		}
	}
	return nil
}

func (s *baseSegment) BatchPkExist(lc *storage.BatchLocationsCache) []bool {
	if s.skipGrowingBF {
		allPositive := make([]bool, lc.Size())
		for i := 0; i < lc.Size(); i++ {
			allPositive[i] = true
		}
		return allPositive
	}
	if s.pkCandidate == nil {
		allPositive := make([]bool, lc.Size())
		for i := 0; i < lc.Size(); i++ {
			allPositive[i] = true
		}
		return allPositive
	}
	return s.pkCandidate.BatchPkExist(lc)
}

// ResourceUsageEstimate returns the final estimated resource usage of the segment.
func (s *baseSegment) ResourceUsageEstimate() ResourceUsage {
	if s.segmentType == SegmentTypeGrowing {
		// Growing segment cannot do resource usage estimate.
		return ResourceUsage{}
	}
	cache := s.resourceUsageCache.Load()
	if cache != nil {
		return *cache
	}

	usage, err := estimateLogicalResourceUsageOfSegment(s.collection.Schema(), s.LoadInfo(), resourceEstimateFactor{
		deltaDataExpansionFactor:        paramtable.Get().QueryNodeCfg.DeltaDataExpansionRate.GetAsFloat(),
		TieredEvictionEnabled:           paramtable.Get().QueryNodeCfg.TieredEvictionEnabled.GetAsBool(),
		TieredEvictableMemoryCacheRatio: paramtable.Get().QueryNodeCfg.TieredEvictableMemoryCacheRatio.GetAsFloat(),
		TieredEvictableDiskCacheRatio:   paramtable.Get().QueryNodeCfg.TieredEvictableDiskCacheRatio.GetAsFloat(),
	})
	if err != nil {
		// Should never failure, if failed, segment should never be loaded.
		mlog.Warn(context.TODO(), "unreachable: failed to get resource usage estimate of segment", mlog.Err(err), mlog.FieldCollectionID(s.Collection()), mlog.FieldSegmentID(s.ID()))
		return ResourceUsage{}
	}
	s.resourceUsageCache.Store(usage)
	return *usage
}

func (s *baseSegment) NeedUpdatedVersion() int64 {
	return s.needUpdatedVersion.Load()
}

func (s *baseSegment) SetNeedUpdatedVersion(version int64) {
	s.needUpdatedVersion.Store(version)
}

type FieldInfo struct {
	*datapb.FieldBinlog
	RowCount int64
}

var _ Segment = (*LocalSegment)(nil)

// Segment is a wrapper of the underlying C-structure segment.
type LocalSegment struct {
	baseSegment
	*bm25StatsHolder

	manager SegmentManager
	ptrLock *state.LoadStateLock
	ptr     C.CSegmentInterface // TODO: Remove in future, after move load index into segcore package.
	// always keep same with csegment.RawPtr(), for eaiser to access,
	csegment segcore.CSegment

	// cached results, to avoid too many CGO calls
	memSize         *atomic.Int64
	binlogSize      *atomic.Int64
	relatedDataSize *atomic.Int64
	rowNum          *atomic.Int64
	insertCount     *atomic.Int64

	deltaMut           sync.Mutex
	lastDeltaTimestamp *atomic.Uint64
	fields             *typeutil.ConcurrentMap[int64, *FieldInfo]
	fieldIndexes       *typeutil.ConcurrentMap[int64, *IndexedFieldInfo] // indexID -> IndexedFieldInfo
	fieldJSONStats     map[int64]*querypb.JsonStatsInfo
	fieldJSONStatsMu   sync.RWMutex
}

func NewSegment(ctx context.Context,
	collection *Collection,
	manager SegmentManager,
	segmentType SegmentType,
	version int64,
	loadInfo *querypb.SegmentLoadInfo,
) (Segment, error) {
	/*
		CStatus
		NewSegment(CCollection collection, uint64_t segment_id, SegmentType seg_type, CSegmentInterface* newSegment);
	*/
	if loadInfo.GetLevel() == datapb.SegmentLevel_L0 {
		return NewL0Segment(collection, segmentType, version, loadInfo)
	}

	base, err := newBaseSegment(collection, segmentType, version, loadInfo)
	if err != nil {
		return nil, err
	}

	var locker *state.LoadStateLock
	switch segmentType {
	case SegmentTypeSealed:
		locker = state.NewLoadStateLock(state.LoadStateOnlyMeta)
	case SegmentTypeGrowing:
		locker = state.NewLoadStateLock(state.LoadStateDataLoaded)
	default:
		return nil, merr.WrapErrServiceInternalMsg("illegal segment type %d when create segment %d", segmentType, loadInfo.GetSegmentID())
	}

	logger := mlog.With(
		mlog.FieldCollectionID(loadInfo.GetCollectionID()),
		mlog.FieldPartitionID(loadInfo.GetPartitionID()),
		mlog.FieldSegmentID(loadInfo.GetSegmentID()),
		mlog.String("segmentType", segmentType.String()),
		mlog.String("level", loadInfo.GetLevel().String()),
	)

	var csegment segcore.CSegment
	if _, err := GetDynamicPool().Submit(func() (any, error) {
		var err error
		csegment, err = collection.CreateCSegment(&segcore.CreateCSegmentRequest{
			SegmentID:   loadInfo.GetSegmentID(),
			SegmentType: segmentType,
			IsSorted:    loadInfo.GetIsSorted(),
			LoadInfo:    loadInfo,
		})
		return nil, err
	}).Await(); err != nil {
		logger.Warn(ctx, "create segment failed", mlog.Err(err))
		return nil, err
	}
	logger.Info(ctx, "create segment done")

	segment := &LocalSegment{
		baseSegment:        base,
		bm25StatsHolder:    newBM25StatsHolder(),
		manager:            manager,
		ptrLock:            locker,
		ptr:                C.CSegmentInterface(csegment.RawPointer()),
		csegment:           csegment,
		lastDeltaTimestamp: atomic.NewUint64(0),
		fields:             typeutil.NewConcurrentMap[int64, *FieldInfo](),
		fieldIndexes:       typeutil.NewConcurrentMap[int64, *IndexedFieldInfo](),
		fieldJSONStats:     make(map[int64]*querypb.JsonStatsInfo),

		memSize:         atomic.NewInt64(-1),
		binlogSize:      atomic.NewInt64(0),
		relatedDataSize: atomic.NewInt64(-1),
		rowNum:          atomic.NewInt64(-1),
		insertCount:     atomic.NewInt64(0),
	}

	if err := segment.initializeSegment(); err != nil {
		csegment.Release()
		return nil, err
	}
	return segment, nil
}

func (s *LocalSegment) initializeSegment() error {
	loadInfo := s.loadInfo.Load()
	indexedFieldInfos, fieldBinlogs := separateIndexAndBinlog(loadInfo)
	schemaHelper, _ := typeutil.CreateSchemaHelper(s.collection.Schema())

	for _, info := range indexedFieldInfos {
		fieldID := info.IndexInfo.FieldID
		field, err := schemaHelper.GetFieldFromID(fieldID)
		if err != nil {
			return err
		}
		indexInfo := info.IndexInfo
		s.fieldIndexes.Insert(indexInfo.GetIndexID(), &IndexedFieldInfo{
			FieldBinlog: &datapb.FieldBinlog{
				FieldID: indexInfo.GetFieldID(),
			},
			IndexInfo: indexInfo,
			IsLoaded:  false,
		})
		if !typeutil.IsVectorType(field.GetDataType()) && !s.HasRawData(fieldID) {
			s.fields.Insert(fieldID, &FieldInfo{
				FieldBinlog: info.FieldBinlog,
				RowCount:    loadInfo.GetNumOfRows(),
			})
		}
	}

	for _, binlogs := range fieldBinlogs {
		s.fields.Insert(binlogs.FieldID, &FieldInfo{
			FieldBinlog: binlogs,
			RowCount:    loadInfo.GetNumOfRows(),
		})
	}

	// Update the insert count when initialize the segment and update the metrics.
	s.insertCount.Store(loadInfo.GetNumOfRows())
	return nil
}

// PinIfNotReleased acquires the `ptrLock` and returns true if the pointer is valid
// Provide ONLY the read lock operations,
// don't make `ptrLock` public to avoid abusing of the mutex.
func (s *LocalSegment) PinIfNotReleased() error {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	return nil
}

func (s *LocalSegment) Unpin() {
	s.ptrLock.Unpin()
}

func (s *LocalSegment) InsertCount() int64 {
	return s.insertCount.Load()
}

func (s *LocalSegment) RowNum() int64 {
	// if segment is not loaded, return 0 (maybe not loaded or release by lru)
	if !s.ptrLock.PinIf(state.IsDataLoaded) {
		return 0
	}
	defer s.ptrLock.Unpin()

	rowNum := s.rowNum.Load()
	if rowNum < 0 {
		GetDynamicPool().Submit(func() (any, error) {
			rowNum = s.csegment.RowNum()
			s.rowNum.Store(rowNum)
			return nil, nil
		}).Await()
	}
	return rowNum
}

func (s *LocalSegment) MemSize() int64 {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return 0
	}
	defer s.ptrLock.Unpin()

	memSize := s.memSize.Load()
	if memSize < 0 {
		GetDynamicPool().Submit(func() (any, error) {
			memSize = s.csegment.MemSize()
			s.memSize.Store(memSize)
			return nil, nil
		}).Await()
	}
	return memSize
}

func (s *LocalSegment) LastDeltaTimestamp() uint64 {
	return s.lastDeltaTimestamp.Load()
}

// advanceLastDeltaTimestamp moves lastDeltaTimestamp forward to max(current, max(tss)).
// Consumers (file-level skip in segment_loader.LoadDeltaLogs, dist_handler reporting to
// QueryCoord) treat this field as a high-water-mark. Using tss[last] on unsorted batches
// underestimates the watermark, so we scan for the true max.
func (s *LocalSegment) advanceLastDeltaTimestamp(tss []typeutil.Timestamp) {
	if len(tss) == 0 {
		return
	}
	maxTs := tss[0]
	for _, t := range tss[1:] {
		if t > maxTs {
			maxTs = t
		}
	}
	for {
		cur := s.lastDeltaTimestamp.Load()
		if maxTs <= cur {
			return
		}
		if s.lastDeltaTimestamp.CompareAndSwap(cur, maxTs) {
			return
		}
	}
}

// UpdatePkCandidate updates the PK candidate with provided pks and charges resource.
// Overrides baseSegment.UpdatePkCandidate to handle resource charging for growing segments.
func (s *LocalSegment) UpdatePkCandidate(pks []storage.PrimaryKey) {
	if s.skipGrowingBF {
		return
	}

	s.pkCandidate.UpdatePkCandidate(pks)

	// Charge resource (safe to call multiple times - only charges once)
	s.pkCandidate.Charge()
}

func (s *LocalSegment) GetIndexByID(indexID int64) *IndexedFieldInfo {
	info, _ := s.fieldIndexes.Get(indexID)
	return info
}

func (s *LocalSegment) GetIndex(fieldID int64) []*IndexedFieldInfo {
	var info []*IndexedFieldInfo
	s.fieldIndexes.Range(func(key int64, value *IndexedFieldInfo) bool {
		if value.IndexInfo.FieldID == fieldID {
			info = append(info, value)
		}
		return true
	})
	return info
}

func (s *LocalSegment) ExistIndex(fieldID int64) bool {
	contain := false
	s.fieldIndexes.Range(func(key int64, value *IndexedFieldInfo) bool {
		if value.IndexInfo.FieldID == fieldID {
			contain = true
		}
		return !contain
	})

	return contain
}

func (s *LocalSegment) HasRawData(fieldID int64) bool {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return false
	}
	defer s.ptrLock.Unpin()

	return s.csegment.HasRawData(fieldID)
}

func (s *LocalSegment) HasFieldData(fieldID int64) bool {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return false
	}
	defer s.ptrLock.Unpin()
	return s.csegment.HasFieldData(fieldID)
}

func (s *LocalSegment) Indexes() []*IndexedFieldInfo {
	var result []*IndexedFieldInfo
	s.fieldIndexes.Range(func(key int64, value *IndexedFieldInfo) bool {
		result = append(result, value)
		return true
	})
	return result
}

func (s *LocalSegment) IsLazyLoad() bool {
	for _, indexInfo := range s.Indexes() {
		if !indexInfo.IsLoaded {
			return true
		}
	}
	return false
}

func (s *LocalSegment) ResetIndexesLazyLoad(lazyState bool) {
	for _, indexInfo := range s.Indexes() {
		indexInfo.IsLoaded = lazyState
	}
}

func (s *LocalSegment) syncFieldIndexes(indexInfos []*querypb.FieldIndexInfo) {
	isLoadedByIndexID := make(map[int64]bool)
	loadedForNewIndex := !s.IsLazyLoad()
	for _, index := range s.Indexes() {
		isLoadedByIndexID[index.IndexInfo.GetIndexID()] = index.IsLoaded
	}

	indexIDs := typeutil.NewSet[int64]()
	for _, indexInfo := range indexInfos {
		indexID := indexInfo.GetIndexID()
		indexIDs.Insert(indexID)
		isLoaded, ok := isLoadedByIndexID[indexID]
		if !ok {
			isLoaded = loadedForNewIndex
		}
		s.fieldIndexes.Insert(indexID, &IndexedFieldInfo{
			FieldBinlog: &datapb.FieldBinlog{
				FieldID: indexInfo.GetFieldID(),
			},
			IndexInfo: indexInfo,
			IsLoaded:  isLoaded,
		})
	}

	// QueryCoord builds reopen LoadInfo from DataCoord's full finished-index list for this segment.
	// Treat absent index IDs as stale local metadata.
	s.fieldIndexes.Range(func(indexID int64, _ *IndexedFieldInfo) bool {
		if !indexIDs.Contain(indexID) {
			s.fieldIndexes.Remove(indexID)
		}
		return true
	})
}

// Search executes a search on the segment.
// If searchReq.FilterOnly() is true, only executes the filter and returns valid_count (Stage 1 of two-stage search).
func (s *LocalSegment) Search(ctx context.Context, searchReq *segcore.SearchRequest) (*segcore.SearchResult, error) {
	filterOnly := searchReq.FilterOnly()
	log := mlog.WithLazy(
		mlog.Uint64("mvcc", searchReq.MVCC()),
		mlog.FieldCollectionID(s.Collection()),
		mlog.FieldSegmentID(s.ID()),
		mlog.String("segmentType", s.segmentType.String()),
		mlog.Bool("filterOnly", filterOnly),
	)

	if !s.ptrLock.PinIf(state.IsNotReleased) {
		// TODO: check if the segment is readable but not released. too many related logic need to be refactor.
		return nil, merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	hasIndex := s.ExistIndex(searchReq.SearchFieldID())
	log = log.With(mlog.Bool("withIndex", hasIndex))
	log.Debug(ctx, "search segment...")

	tr := timerecord.NewTimeRecorder("cgoSearch")
	result, err := s.csegment.Search(ctx, searchReq)
	if err != nil {
		log.Warn(ctx, "Search failed")
		return nil, err
	}
	metrics.QueryNodeSQSegmentLatencyInCore.WithLabelValues(paramtable.GetStringNodeID(), metrics.SearchLabel).Observe(float64(tr.ElapseSpan().Microseconds()) / 1000.0)
	if filterOnly {
		log.Debug(ctx, "search filter only segment done", mlog.Int64("validCount", result.ValidCount()))
	} else {
		log.Debug(ctx, "search segment done")
	}
	return result, nil
}

func (s *LocalSegment) retrieve(ctx context.Context, plan *segcore.RetrievePlan, log *mlog.Logger) (*segcore.RetrieveResult, error) {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		// TODO: check if the segment is readable but not released. too many related logic need to be refactor.
		return nil, merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	log.Debug(ctx, "begin to retrieve")

	tr := timerecord.NewTimeRecorder("cgoRetrieve")
	result, err := retrySegmentReadGate(
		ctx,
		s.segmentType,
		func() (*segcore.RetrieveResult, error) {
			return s.csegment.Retrieve(ctx, plan)
		},
		waitSegmentReadGateRetry,
	)
	if err != nil {
		log.Warn(ctx, "Retrieve failed")
		return nil, err
	}
	metrics.QueryNodeSQSegmentLatencyInCore.WithLabelValues(paramtable.GetStringNodeID(),
		contextutil.GetQueryLabel(ctx)).Observe(float64(tr.ElapseSpan().Microseconds()) / 1000.0)
	return result, nil
}

func (s *LocalSegment) Retrieve(ctx context.Context, plan *segcore.RetrievePlan) (*segcorepb.RetrieveResults, error) {
	log := mlog.WithLazy(
		mlog.FieldCollectionID(s.Collection()),
		mlog.FieldPartitionID(s.Partition()),
		mlog.FieldSegmentID(s.ID()),
		mlog.Uint64("mvcc", plan.Timestamp),
		mlog.String("segmentType", s.segmentType.String()),
	)

	result, err := s.retrieve(ctx, plan, log)
	if err != nil {
		return nil, err
	}
	defer result.Release()

	_, span := otel.Tracer(typeutil.QueryNodeRole).Start(ctx, "partial-segcore-results-deserialization")
	defer span.End()

	retrieveResult, err := result.GetResult()
	if err != nil {
		log.Warn(ctx, "unmarshal retrieve result failed", mlog.Err(err))
		return nil, err
	}
	log.Debug(ctx, "retrieve segment done", mlog.Int("resultNum", len(retrieveResult.Offset)))
	return retrieveResult, nil
}

func (s *LocalSegment) retrieveByOffsets(ctx context.Context, plan *segcore.RetrievePlanWithOffsets, log *mlog.Logger) (*segcore.RetrieveResult, error) {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		// TODO: check if the segment is readable but not released. too many related logic need to be refactor.
		return nil, merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	log.Debug(ctx, "begin to retrieve by offsets")
	tr := timerecord.NewTimeRecorder("cgoRetrieveByOffsets")
	result, err := retrySegmentReadGate(
		ctx,
		s.segmentType,
		func() (*segcore.RetrieveResult, error) {
			return s.csegment.RetrieveByOffsets(ctx, plan)
		},
		waitSegmentReadGateRetry,
	)
	if err != nil {
		log.Warn(ctx, "RetrieveByOffsets failed")
		return nil, err
	}
	metrics.QueryNodeSQSegmentLatencyInCore.WithLabelValues(paramtable.GetStringNodeID(),
		contextutil.GetQueryLabel(ctx)).Observe(float64(tr.ElapseSpan().Microseconds()) / 1000.0)
	return result, nil
}

func (s *LocalSegment) RetrieveByOffsets(ctx context.Context, plan *segcore.RetrievePlanWithOffsets) (*segcorepb.RetrieveResults, error) {
	log := mlog.WithLazy(mlog.FieldCollectionID(s.Collection()),
		mlog.FieldPartitionID(s.Partition()),
		mlog.FieldSegmentID(s.ID()),
		mlog.Int64("msgID", plan.MsgID()),
		mlog.String("segmentType", s.segmentType.String()),
		mlog.Int("resultNum", len(plan.Offsets)),
	)

	result, err := s.retrieveByOffsets(ctx, plan, log)
	if err != nil {
		return nil, err
	}
	defer result.Release()

	_, span := otel.Tracer(typeutil.QueryNodeRole).Start(ctx, "reduced-segcore-results-deserialization")
	defer span.End()

	retrieveResult, err := result.GetResult()
	if err != nil {
		log.Warn(ctx, "unmarshal retrieve by offsets result failed", mlog.Err(err))
		return nil, err
	}
	log.Debug(ctx, "retrieve by segment offsets done", mlog.Int("resultNum", len(retrieveResult.Offset)))
	return retrieveResult, nil
}

func (s *LocalSegment) Insert(ctx context.Context, rowIDs []int64, timestamps []typeutil.Timestamp, record *segcorepb.InsertRecord) error {
	if s.Type() != SegmentTypeGrowing {
		return merr.WrapErrServiceInternalMsg("unexpected segmentType when segmentInsert, segmentType = %s", s.segmentType.String())
	}
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	var result *segcore.InsertResult
	var err error
	GetMutatePool().Submit(func() (any, error) {
		start := time.Now()
		defer func() {
			metrics.QueryNodeCGOCallLatency.WithLabelValues(
				paramtable.GetStringNodeID(),
				"Insert",
				"Sync",
			).Observe(float64(time.Since(start).Milliseconds()))
		}()

		result, err = s.csegment.Insert(ctx, &segcore.InsertRequest{
			RowIDs:     rowIDs,
			Timestamps: timestamps,
			Record:     record,
		})
		return nil, nil
	}).Await()

	if err != nil {
		return err
	}
	s.insertCount.Add(result.InsertedRows)
	s.rowNum.Store(-1)
	s.memSize.Store(-1)
	return nil
}

func (s *LocalSegment) Delete(ctx context.Context, primaryKeys storage.PrimaryKeys, timestamps []typeutil.Timestamp) error {
	/*
		CStatus
		Delete(CSegmentInterface c_segment,
		           long int reserved_offset,
		           long size,
		           const long* primary_keys,
		           const unsigned long* timestamps);
	*/

	if primaryKeys.Len() == 0 {
		return nil
	}
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	s.deltaMut.Lock()
	defer s.deltaMut.Unlock()

	// segcore DeletedRecord::InternalPush is idempotent on (PK, ts):
	// duplicate deletes against already-deleted rows are discarded internally.
	// Do NOT add a ts-watermark skip here. In L0-forward + partial-L0-compaction
	// scenarios, batches contain ts values below the watermark that have NOT yet
	// been applied to this segment, and skipping them causes silent data loss.

	var err error
	GetMutatePool().Submit(func() (any, error) {
		start := time.Now()
		defer func() {
			metrics.QueryNodeCGOCallLatency.WithLabelValues(
				paramtable.GetStringNodeID(),
				"Delete",
				"Sync",
			).Observe(float64(time.Since(start).Milliseconds()))
		}()
		_, err = s.csegment.Delete(ctx, &segcore.DeleteRequest{
			PrimaryKeys: primaryKeys,
			Timestamps:  timestamps,
		})
		return nil, nil
	}).Await()

	if err != nil {
		return err
	}

	s.rowNum.Store(-1)
	// Track max ts as a high-water-mark (consumed by file-level skip in
	// LoadDeltaLogs and by dist_handler for QueryCoord reporting). Using
	// tss[last] on unsorted batches underestimates the watermark.
	s.advanceLastDeltaTimestamp(timestamps)
	return nil
}

// -------------------------------------------------------------------------------------- interfaces for sealed segment
func (s *LocalSegment) LoadFieldData(ctx context.Context, fieldID int64, rowCount int64, field *datapb.FieldBinlog) error {
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	ctx, sp := otel.Tracer(typeutil.QueryNodeRole).Start(ctx, fmt.Sprintf("LoadFieldData-%d-%d", s.ID(), fieldID))
	defer sp.End()

	log := mlog.With(
		mlog.FieldCollectionID(s.Collection()),
		mlog.FieldPartitionID(s.Partition()),
		mlog.FieldSegmentID(s.ID()),
		mlog.FieldFieldID(fieldID),
		mlog.Int64("rowCount", rowCount),
	)
	log.Info(ctx, "start loading field data for field")

	// TODO retrieve_enable should be considered
	collection := s.collection
	fieldSchema, err := getFieldSchema(collection.Schema(), fieldID)
	if err != nil {
		return err
	}
	mmapEnabled := isDataMmapEnable(fieldSchema)
	fieldWarmupPolicy := getFieldWarmupPolicy(fieldSchema)

	req := &segcore.LoadFieldDataRequest{
		Fields: []segcore.LoadFieldDataInfo{{
			Field:        field,
			EnableMMap:   mmapEnabled,
			WarmupPolicy: fieldWarmupPolicy,
		}},
		RowCount:       rowCount,
		StorageVersion: s.LoadInfo().GetStorageVersion(),
		LoadPriority:   s.LoadInfo().GetPriority(),
		Shard:          s.LoadInfo().GetInsertChannel(),
	}

	GetLoadPool().Submit(func() (any, error) {
		start := time.Now()
		defer func() {
			metrics.QueryNodeCGOCallLatency.WithLabelValues(
				paramtable.GetStringNodeID(),
				"LoadFieldData",
				"Sync",
			).Observe(float64(time.Since(start).Milliseconds()))
		}()
		_, err = s.csegment.LoadFieldData(ctx, req)
		log.Info(ctx, "submitted loadFieldData task to load pool")
		return nil, nil
	}).Await()

	if err != nil {
		log.Warn(ctx, "LoadFieldData failed", mlog.Err(err))
		return err
	}
	log.Info(ctx, "load field done")
	return nil
}

func (s *LocalSegment) LoadDeltaData(ctx context.Context, deltaData *storage.DeltaData) error {
	if deltaData.DeleteRowCount() == 0 {
		return nil
	}

	pks, tss := deltaData.DeletePks(), deltaData.DeleteTimestamps()
	rowNum := deltaData.DeleteRowCount()

	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	log := mlog.With(
		mlog.FieldCollectionID(s.Collection()),
		mlog.FieldPartitionID(s.Partition()),
		mlog.FieldSegmentID(s.ID()),
	)

	s.deltaMut.Lock()
	defer s.deltaMut.Unlock()

	// See comment in Delete(): segcore dedups at (PK, ts) level, and tss is
	// NOT sorted across L0 segments (BufferForwarder appends in iteration
	// order), so comparing against tss[last] is both unnecessary and incorrect.

	ids, err := storage.ParsePrimaryKeysBatch2IDs(pks)
	if err != nil {
		return err
	}

	idsBlob, err := proto.Marshal(ids)
	if err != nil {
		return err
	}

	loadInfo := C.CLoadDeletedRecordInfo{
		timestamps:        unsafe.Pointer(&tss[0]),
		primary_keys:      (*C.uint8_t)(unsafe.Pointer(&idsBlob[0])),
		primary_keys_size: C.uint64_t(len(idsBlob)),
		row_count:         C.int64_t(rowNum),
	}
	/*
		CStatus
		LoadDeletedRecord(CSegmentInterface c_segment, CLoadDeletedRecordInfo deleted_record_info)
	*/
	var status C.CStatus
	// Delta-log replay during segment load runs on the load pool, not the
	// online-write mutate pool, so a large post-compaction replay cannot starve
	// online insert/delete (and thus tSafe advancement).
	GetLoadPool().Submit(func() (any, error) {
		start := time.Now()
		defer func() {
			metrics.QueryNodeCGOCallLatency.WithLabelValues(
				paramtable.GetStringNodeID(),
				"LoadDeletedRecord",
				"Sync",
			).Observe(float64(time.Since(start).Milliseconds()))
		}()
		status = C.LoadDeletedRecord(s.ptr, loadInfo)
		return nil, nil
	}).Await()

	if err := HandleCStatus(ctx, &status, "LoadDeletedRecord failed",
		mlog.FieldCollectionID(s.Collection()),
		mlog.FieldPartitionID(s.Partition()),
		mlog.FieldSegmentID(s.ID())); err != nil {
		return err
	}

	s.rowNum.Store(-1)
	s.advanceLastDeltaTimestamp(tss)

	log.Info(ctx, "load deleted record done",
		mlog.Int64("rowNum", rowNum),
		mlog.String("segmentType", s.Type().String()))
	return nil
}

func GetCLoadInfoWithFunc(ctx context.Context,
	fieldSchema *schemapb.FieldSchema,
	loadInfo *querypb.SegmentLoadInfo,
	indexInfo *querypb.FieldIndexInfo,
	f func(c *LoadIndexInfo) error,
) error {
	// 1.
	loadIndexInfo, err := newLoadIndexInfo(ctx)
	if err != nil {
		return err
	}
	defer deleteLoadIndexInfo(loadIndexInfo)

	indexParams := funcutil.KeyValuePair2Map(indexInfo.IndexParams)
	// as Knowhere reports error if encounter an unknown param, we need to delete it
	delete(indexParams, common.MmapEnabledKey)

	// some build params also exist in indexParams, which are useless during loading process
	if vecindexmgr.GetVecIndexMgrInstance().IsDiskANN(indexParams["index_type"]) {
		if err := indexparams.SetDiskIndexLoadParams(paramtable.Get(), indexParams, indexInfo.GetNumRows()); err != nil {
			return err
		}
	}

	// set whether enable offset cache for bitmap index
	if indexParams["index_type"] == indexparamcheck.IndexBitmap {
		indexparams.SetBitmapIndexLoadParams(paramtable.Get(), indexParams)
	}

	if err := indexparams.AppendPrepareLoadParams(paramtable.Get(), indexParams); err != nil {
		return err
	}

	enableMmap := isIndexMmapEnable(fieldSchema, indexInfo)
	// Add warmup policy to index_params if not already present
	// C++ will pass it to Knowhere for index loading
	if existingWarmup, exists := indexParams[common.WarmupKey]; exists {
		mlog.Info(ctx, "warmup policy already in index params (from QueryCoord)",
			mlog.FieldSegmentID(loadInfo.GetSegmentID()),
			mlog.FieldFieldID(indexInfo.GetFieldID()),
			mlog.String("warmup", existingWarmup))
	} else {
		warmupPolicy := getIndexWarmupPolicy(fieldSchema, indexInfo)
		mlog.Info(ctx, "warmup policy from getIndexWarmupPolicy",
			mlog.FieldSegmentID(loadInfo.GetSegmentID()),
			mlog.FieldFieldID(indexInfo.GetFieldID()),
			mlog.String("warmup", warmupPolicy))
		if warmupPolicy != "" {
			indexParams[common.WarmupKey] = warmupPolicy
		}
	}
	// Pass DataCoord-built index file paths through; QueryNode should not
	// attach v0/v1 path layout semantics to the read path.
	indexInfoProto := &cgopb.LoadIndexInfo{
		CollectionID:              loadInfo.GetCollectionID(),
		PartitionID:               loadInfo.GetPartitionID(),
		SegmentID:                 loadInfo.GetSegmentID(),
		Field:                     fieldSchema,
		EnableMmap:                enableMmap,
		IndexID:                   indexInfo.GetIndexID(),
		IndexBuildID:              indexInfo.GetBuildID(),
		IndexVersion:              indexInfo.GetIndexVersion(),
		IndexParams:               indexParams,
		IndexFiles:                indexInfo.GetIndexFilePaths(),
		IndexEngineVersion:        indexInfo.GetCurrentIndexVersion(),
		IndexFileSize:             indexInfo.GetIndexSize(),
		NumRows:                   indexInfo.GetNumRows(),
		CurrentScalarIndexVersion: indexInfo.GetCurrentScalarIndexVersion(),
		IndexStorePathVersion:     indexInfo.GetIndexStorePathVersion(),
	}

	// 2.
	if err := loadIndexInfo.appendLoadIndexInfo(ctx, indexInfoProto); err != nil {
		mlog.Warn(ctx, "fail to append load index info", mlog.Err(err))
		return err
	}
	loadIndexInfo.setShard(loadInfo.GetInsertChannel())
	return f(loadIndexInfo)
}

func (s *LocalSegment) syncFieldJSONStatsFromLoadInfo(ctx context.Context, loadInfo *querypb.SegmentLoadInfo) {
	jsonStatsInfo := make(map[int64]*querypb.JsonStatsInfo)
	if !paramtable.Get().CommonCfg.EnabledJSONKeyStats.GetAsBool() {
		mlog.Warn(ctx, "skip sync json key stats, json key stats is not enabled", mlog.FieldSegmentID(s.ID()))
		s.fieldJSONStatsMu.Lock()
		s.fieldJSONStats = jsonStatsInfo
		s.fieldJSONStatsMu.Unlock()
		return
	}

	statsResult := packed.NewStatsResolverFromLoadInfo(loadInfo).TextAndJSONIndexStatsWithBasePaths()
	jsonKeyStats := statsResult.JSONKeyStats
	if statsResult.Err() != nil {
		mlog.Warn(ctx, "failed to resolve json key stats from manifest",
			mlog.FieldSegmentID(loadInfo.GetSegmentID()),
			mlog.String("manifestPath", loadInfo.GetManifestPath()),
			mlog.Err(statsResult.Err()))
		jsonKeyStats = loadInfo.GetJsonKeyStatsLogs()
	}

	for fieldID, stats := range jsonKeyStats {
		if stats == nil {
			continue
		}
		if stats.GetJsonKeyStatsDataFormat() != common.JSONStatsDataFormatVersion {
			mlog.Warn(ctx, "skip sync json key stats, data format invalid",
				mlog.FieldSegmentID(loadInfo.GetSegmentID()),
				mlog.FieldFieldID(fieldID),
				mlog.FieldBuildID(stats.GetBuildID()),
				mlog.Int64("version", stats.GetVersion()),
				mlog.Int64("dataFormat", stats.GetJsonKeyStatsDataFormat()),
				mlog.Int64("expectedDataFormat", common.JSONStatsDataFormatVersion))
			continue
		}
		jsonStatsInfo[fieldID] = &querypb.JsonStatsInfo{
			FieldID:           stats.GetFieldID(),
			DataFormatVersion: stats.GetJsonKeyStatsDataFormat(),
			BuildID:           stats.GetBuildID(),
			VersionID:         stats.GetVersion(),
		}
	}

	s.fieldJSONStatsMu.Lock()
	s.fieldJSONStats = jsonStatsInfo
	s.fieldJSONStatsMu.Unlock()
}

func (s *LocalSegment) Load(ctx context.Context) error {
	if err := s.csegment.Load(ctx); err != nil {
		return err
	}
	s.syncFieldJSONStatsFromLoadInfo(ctx, s.LoadInfo())
	return nil
}

func (s *LocalSegment) Reopen(ctx context.Context, newLoadInfo *querypb.SegmentLoadInfo) error {
	if !s.ptrLock.PinIfNotReleased() {
		return merr.WrapErrSegmentNotLoaded(s.ID(), "segment released during reopen")
	}
	defer s.ptrLock.Unpin()

	// Reopen forwards the SegmentLoadInfo straight to segcore, so it must inject
	// the QueryNode-local index load params (e.g. DISKANN num_load_thread) that
	// the full-load path injects; otherwise segcore asserts on load. See #51249.
	if err := prepareIndexLoadParams(newLoadInfo.GetIndexInfos()); err != nil {
		return err
	}

	schema, schemaVersion := s.collection.SchemaAndSegcoreVersion()
	err := s.csegment.Reopen(ctx, &segcore.ReopenRequest{
		LoadInfo:      newLoadInfo,
		Schema:        schema,
		SchemaVersion: schemaVersion,
	})
	if err != nil {
		return err
	}
	s.syncFieldIndexes(newLoadInfo.GetIndexInfos())
	if s.relatedDataSize != nil {
		s.relatedDataSize.Store(calculateSegmentLogSize(newLoadInfo))
	}
	s.loadInfo.Store(newLoadInfo)
	s.syncFieldJSONStatsFromLoadInfo(ctx, newLoadInfo)
	s.compactLoadInfoForRuntime()
	return nil
}

type ReleaseScope int

const (
	ReleaseScopeAll ReleaseScope = iota
	ReleaseScopeData
)

type releaseOptions struct {
	Scope ReleaseScope
}

func newReleaseOptions() *releaseOptions {
	return &releaseOptions{
		Scope: ReleaseScopeAll,
	}
}

type releaseOption func(*releaseOptions)

func WithReleaseScope(scope ReleaseScope) releaseOption {
	return func(options *releaseOptions) {
		options.Scope = scope
	}
}

func (s *LocalSegment) Release(ctx context.Context, opts ...releaseOption) {
	options := newReleaseOptions()
	for _, opt := range opts {
		opt(options)
	}
	stateLockGuard := s.startRelease(options.Scope)
	if stateLockGuard == nil { // release is already done.
		return
	}
	// release will never fail
	defer stateLockGuard.Done(nil)

	log := mlog.With(mlog.FieldCollectionID(s.Collection()),
		mlog.FieldPartitionID(s.Partition()),
		mlog.FieldSegmentID(s.ID()),
		mlog.String("segmentType", s.segmentType.String()),
		mlog.Int64("insertCount", s.InsertCount()),
	)

	// wait all read ops finished
	ptr := s.ptr
	if options.Scope == ReleaseScopeData {
		s.ReleaseSegmentData()
		log.Info(ctx, "release segment data done and the field indexes info has been set lazy load=true")
		return
	}

	if paramtable.Get().QueryNodeCfg.ExprResCacheEnabled.GetAsBool() {
		// erase expr-cache for this segment before deleting C segment
		C.ExprResCacheEraseSegment(C.int64_t(s.ID()))
	}

	GetDynamicPool().Submit(func() (any, error) {
		C.DeleteSegment(ptr)
		return nil, nil
	}).Await()

	// TODO: disable logical resource handling for now
	// usage := s.ResourceUsageEstimate()
	// s.manager.SubLogicalResource(usage)

	// Refund PK candidate resource
	s.pkCandidate.Refund()

	binlogSize := s.binlogSize.Load()
	if binlogSize > 0 {
		// no concurrent change to s.binlogSize, so the subtraction is safe
		s.manager.SubLoadedBinlogSize(binlogSize)
		s.binlogSize.Store(0)
	}

	log.Info(ctx, "delete segment from memory")
}

// ReleaseSegmentData releases the segment data.
func (s *LocalSegment) ReleaseSegmentData() {
	GetDynamicPool().Submit(func() (any, error) {
		C.ClearSegmentData(s.ptr)
		return nil, nil
	}).Await()
	for _, indexInfo := range s.Indexes() {
		indexInfo.IsLoaded = false
	}
}

// StartLoadData starts the loading process of the segment.
func (s *LocalSegment) StartLoadData() (state.LoadStateLockGuard, error) {
	return s.ptrLock.StartLoadData()
}

// startRelease starts the releasing process of the segment.
func (s *LocalSegment) startRelease(scope ReleaseScope) state.LoadStateLockGuard {
	switch scope {
	case ReleaseScopeData:
		return s.ptrLock.StartReleaseData()
	case ReleaseScopeAll:
		return s.ptrLock.StartReleaseAll()
	default:
		panic(fmt.Sprintf("unexpected release scope %d", scope))
	}
}

func (s *LocalSegment) RemoveFieldFile(fieldId int64) {
	GetDynamicPool().Submit(func() (any, error) {
		C.RemoveFieldFile(s.ptr, C.int64_t(fieldId))
		return nil, nil
	}).Await()
}

func (s *LocalSegment) GetFieldJSONIndexStats() map[int64]*querypb.JsonStatsInfo {
	s.fieldJSONStatsMu.RLock()
	defer s.fieldJSONStatsMu.RUnlock()

	stats := make(map[int64]*querypb.JsonStatsInfo, len(s.fieldJSONStats))
	for fieldID, info := range s.fieldJSONStats {
		if info == nil {
			continue
		}
		stats[fieldID] = proto.Clone(info).(*querypb.JsonStatsInfo)
	}
	return stats
}

// MaterializedFieldIDs returns the field ids with materialized columns in the
// growing segment's insert record. The flush layout must be trimmed to this
// set; a non-materialized column is legally absent (a dropped field or a
// function output backfilled by bump-schema compaction).
func (s *LocalSegment) MaterializedFieldIDs(ctx context.Context) ([]int64, error) {
	if s.Type() != SegmentTypeGrowing {
		return nil, merr.WrapErrServiceInternalMsg("unexpected segmentType for MaterializedFieldIDs, segmentType = %s", s.segmentType.String())
	}
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return nil, merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	var cIDs *C.int64_t
	var cCount C.int64_t
	var status C.CStatus
	GetDynamicPool().Submit(func() (any, error) {
		status = C.GetGrowingSegmentMaterializedFieldIDs(s.ptr, &cIDs, &cCount)
		return nil, nil
	}).Await()
	if err := HandleCStatus(ctx, &status, "GetGrowingSegmentMaterializedFieldIDs"); err != nil {
		return nil, err
	}
	if cIDs == nil || cCount == 0 {
		return nil, nil
	}
	defer C.free(unsafe.Pointer(cIDs))
	ids := make([]int64, 0, int(cCount))
	for _, id := range unsafe.Slice(cIDs, int(cCount)) {
		ids = append(ids, int64(id))
	}
	return ids, nil
}

func (s *LocalSegment) PrimaryKeys(ctx context.Context, startOffset, endOffset int64) ([]storage.PrimaryKey, error) {
	if s.Type() != SegmentTypeGrowing {
		return nil, merr.WrapErrServiceInternalMsg("unexpected segmentType for PrimaryKeys, segmentType = %s", s.segmentType.String())
	}
	if startOffset < 0 || endOffset < startOffset {
		return nil, merr.WrapErrServiceInternalMsg("invalid primary key offsets: start=%d end=%d", startOffset, endOffset)
	}
	if startOffset == endOffset {
		return nil, nil
	}
	if !s.ptrLock.PinIf(state.IsNotReleased) {
		return nil, merr.WrapErrSegmentNotLoaded(s.ID(), "segment released")
	}
	defer s.ptrLock.Unpin()

	var cResult C.CPrimaryKeysResult
	var status C.CStatus
	GetDynamicPool().Submit(func() (any, error) {
		status = C.GetGrowingSegmentPrimaryKeys(
			s.ptr,
			C.int64_t(startOffset),
			C.int64_t(endOffset),
			&cResult,
		)
		return nil, nil
	}).Await()
	defer C.FreePrimaryKeysResult(&cResult)
	if err := HandleCStatus(ctx, &status, "GetGrowingSegmentPrimaryKeys"); err != nil {
		return nil, err
	}
	if int64(cResult.num_primary_keys) != endOffset-startOffset {
		return nil, merr.WrapErrDataIntegrityMsg(
			"growing source primary key count mismatch, expected=%d actual=%d",
			endOffset-startOffset, cResult.num_primary_keys)
	}

	pks := make([]storage.PrimaryKey, 0, int(cResult.num_primary_keys))
	switch schemapb.DataType(cResult.pk_data_type) {
	case schemapb.DataType_Int64:
		if cResult.int64_primary_keys == nil {
			return nil, merr.WrapErrDataIntegrityMsg("growing source int64 primary key data is nil")
		}
		values := unsafe.Slice(cResult.int64_primary_keys, int(cResult.num_primary_keys))
		for _, value := range values {
			pks = append(pks, storage.NewInt64PrimaryKey(int64(value)))
		}
	case schemapb.DataType_VarChar:
		if cResult.varchar_primary_key_offsets == nil {
			return nil, merr.WrapErrDataIntegrityMsg("growing source varchar primary key offsets are nil")
		}
		offsets := unsafe.Slice(cResult.varchar_primary_key_offsets, int(cResult.num_primary_keys)+1)
		var data []byte
		if cResult.varchar_primary_keys_size > 0 {
			if cResult.varchar_primary_keys == nil {
				return nil, merr.WrapErrDataIntegrityMsg("growing source varchar primary key data is nil")
			}
			data = unsafe.Slice((*byte)(unsafe.Pointer(cResult.varchar_primary_keys)), int(cResult.varchar_primary_keys_size))
		}
		for i := 0; i < int(cResult.num_primary_keys); i++ {
			start := int(offsets[i])
			end := int(offsets[i+1])
			if start < 0 || end < start || end > len(data) {
				return nil, merr.WrapErrDataIntegrityMsg(
					"growing source varchar primary key offset out of range, index=%d start=%d end=%d size=%d",
					i, start, end, len(data))
			}
			pks = append(pks, storage.NewVarCharPrimaryKey(string(data[start:end])))
		}
	default:
		return nil, merr.WrapErrServiceInternalMsg("unsupported growing source primary key data type %s",
			schemapb.DataType(cResult.pk_data_type).String())
	}
	return pks, nil
}

// FlushData flushes data from the growing segment directly to storage via C++ milvus-storage.
// This is a unified interface that combines data extraction from segcore and writing to storage.
// The C++ side handles: extracting raw field data from ConcurrentVector, converting to Arrow,
// and writing to storage via milvus-storage with TEXT column LOB handling.
func (s *LocalSegment) FlushData(ctx context.Context, startOffset, endOffset int64, config *FlushConfig) (*FlushResult, error) {
	// currently only growing segments support FlushData
	if s.Type() != SegmentTypeGrowing {
		return nil, merr.WrapErrServiceInternalMsg("FlushData is only supported for growing segments, got %s", s.Type().String())
	}

	// validate offsets
	if startOffset < 0 || endOffset < startOffset {
		return nil, merr.WrapErrServiceInternalMsg("invalid offsets: start=%d, end=%d", startOffset, endOffset)
	}

	// no data to flush
	if startOffset == endOffset {
		return nil, nil
	}
	if config == nil {
		return nil, merr.WrapErrServiceInternalMsg("flush config is nil")
	}
	if config.Schema == nil {
		return nil, merr.WrapErrServiceInternalMsg("flush schema is nil")
	}

	schemaBlob, err := proto.Marshal(config.Schema)
	if err != nil {
		return nil, merr.WrapErrServiceInternalMsg("marshal flush schema failed: %s", err.Error())
	}
	if len(schemaBlob) == 0 {
		return nil, merr.WrapErrServiceInternalMsg("marshal flush schema returned empty blob")
	}

	// build C flush config
	var cConfig C.CFlushConfig
	cSegmentPath := C.CString(config.SegmentBasePath)
	defer C.free(unsafe.Pointer(cSegmentPath))
	cConfig.segment_path = cSegmentPath
	cSchemaBlob := C.CBytes(schemaBlob)
	defer C.free(cSchemaBlob)
	cConfig.schema_blob = cSchemaBlob
	cConfig.schema_length = C.int64_t(len(schemaBlob))

	cConfig.read_version = C.int64_t(config.ReadVersion)
	cConfig.retry_limit = C.uint32_t(3)
	writerFormat := config.WriterFormat
	if writerFormat != "" {
		cWriterFormat := C.CString(writerFormat)
		defer C.free(unsafe.Pointer(cWriterFormat))
		cConfig.writer_format = cWriterFormat
	}
	schemaBasedPattern := config.SchemaBasedPattern
	if schemaBasedPattern != "" {
		cSchemaBasedPattern := C.CString(schemaBasedPattern)
		defer C.free(unsafe.Pointer(cSchemaBasedPattern))
		cConfig.schema_based_pattern = cSchemaBasedPattern
	}
	schemaBasedFormats := config.SchemaBasedFormats
	if schemaBasedFormats != "" {
		cSchemaBasedFormats := C.CString(schemaBasedFormats)
		defer C.free(unsafe.Pointer(cSchemaBasedFormats))
		cConfig.schema_based_formats = cSchemaBasedFormats
	}
	numAllowedFields := len(config.AllowedFieldIDs)
	if numAllowedFields > 0 {
		cAllowedFieldIDs := (*C.int64_t)(C.malloc(C.size_t(numAllowedFields) * C.size_t(unsafe.Sizeof(C.int64_t(0)))))
		defer C.free(unsafe.Pointer(cAllowedFieldIDs))
		allowedFieldIDSlice := unsafe.Slice(cAllowedFieldIDs, numAllowedFields)
		for i, fieldID := range config.AllowedFieldIDs {
			allowedFieldIDSlice[i] = C.int64_t(fieldID)
		}
		cConfig.allowed_field_ids = cAllowedFieldIDs
		cConfig.num_allowed_fields = C.size_t(numAllowedFields)
	}
	numColumnGroups := len(config.ColumnGroups)
	if numColumnGroups > 0 {
		totalFieldCount := 0
		for _, columnGroup := range config.ColumnGroups {
			totalFieldCount += len(columnGroup.Fields)
		}
		cColumnGroupIDs := (*C.int64_t)(C.malloc(C.size_t(numColumnGroups) * C.size_t(unsafe.Sizeof(C.int64_t(0)))))
		defer C.free(unsafe.Pointer(cColumnGroupIDs))
		cColumnGroupFieldCounts := (*C.size_t)(C.malloc(C.size_t(numColumnGroups) * C.size_t(unsafe.Sizeof(C.size_t(0)))))
		defer C.free(unsafe.Pointer(cColumnGroupFieldCounts))
		var cColumnGroupFieldIDs *C.int64_t
		if totalFieldCount > 0 {
			cColumnGroupFieldIDs = (*C.int64_t)(C.malloc(C.size_t(totalFieldCount) * C.size_t(unsafe.Sizeof(C.int64_t(0)))))
			defer C.free(unsafe.Pointer(cColumnGroupFieldIDs))
		}
		groupIDSlice := unsafe.Slice(cColumnGroupIDs, numColumnGroups)
		fieldCountSlice := unsafe.Slice(cColumnGroupFieldCounts, numColumnGroups)
		var fieldIDSlice []C.int64_t
		if totalFieldCount > 0 {
			fieldIDSlice = unsafe.Slice(cColumnGroupFieldIDs, totalFieldCount)
		}
		fieldOffset := 0
		for i, columnGroup := range config.ColumnGroups {
			groupIDSlice[i] = C.int64_t(columnGroup.GroupID)
			fieldCountSlice[i] = C.size_t(len(columnGroup.Fields))
			for _, fieldID := range columnGroup.Fields {
				fieldIDSlice[fieldOffset] = C.int64_t(fieldID)
				fieldOffset++
			}
		}
		cConfig.column_group_ids = cColumnGroupIDs
		cConfig.column_group_field_ids = cColumnGroupFieldIDs
		cConfig.column_group_field_counts = cColumnGroupFieldCounts
		cConfig.num_column_groups = C.size_t(numColumnGroups)
	}

	// populate TEXT column configs
	// All arrays must be C-allocated to avoid "Go pointer to unpinned Go pointer" panic
	numTextCols := len(config.TextFieldIDs)
	if numTextCols > 0 {
		// allocate C arrays via C.malloc (not Go slices) to satisfy CGO pointer rules
		cFieldIDs := (*C.int64_t)(C.malloc(C.size_t(numTextCols) * C.size_t(unsafe.Sizeof(C.int64_t(0)))))
		defer C.free(unsafe.Pointer(cFieldIDs))

		cLobPathPtrs := (**C.char)(C.malloc(C.size_t(numTextCols) * C.size_t(unsafe.Sizeof((*C.char)(nil)))))
		defer C.free(unsafe.Pointer(cLobPathPtrs))

		fieldIDSlice := unsafe.Slice(cFieldIDs, numTextCols)
		lobPathSlice := unsafe.Slice(cLobPathPtrs, numTextCols)
		for i := 0; i < numTextCols; i++ {
			fieldIDSlice[i] = C.int64_t(config.TextFieldIDs[i])
			lobPathSlice[i] = C.CString(config.TextLobPaths[i])
			defer C.free(unsafe.Pointer(lobPathSlice[i]))
		}

		cConfig.text_field_ids = cFieldIDs
		cConfig.text_lob_paths = cLobPathPtrs
		cConfig.text_inline_threshold = C.int64_t(config.TextInlineThreshold)
		cConfig.text_max_lob_file_bytes = C.int64_t(config.TextMaxLobFileBytes)
		cConfig.text_flush_threshold_bytes = C.int64_t(config.TextFlushThresholdBytes)
		cConfig.num_text_columns = C.size_t(numTextCols)
	} else {
		cConfig.text_field_ids = nil
		cConfig.text_lob_paths = nil
		cConfig.num_text_columns = 0
	}
	numBM25Fields := len(config.BM25FieldIDs)
	if numBM25Fields > 0 {
		if len(config.BM25StatsLogIDs) != numBM25Fields {
			return nil, merr.WrapErrServiceInternalMsg("BM25 stats log IDs count mismatch, fields=%d logIDs=%d", numBM25Fields, len(config.BM25StatsLogIDs))
		}
		cBM25FieldIDs := (*C.int64_t)(C.malloc(C.size_t(numBM25Fields) * C.size_t(unsafe.Sizeof(C.int64_t(0)))))
		defer C.free(unsafe.Pointer(cBM25FieldIDs))
		cBM25StatsLogIDs := (*C.int64_t)(C.malloc(C.size_t(numBM25Fields) * C.size_t(unsafe.Sizeof(C.int64_t(0)))))
		defer C.free(unsafe.Pointer(cBM25StatsLogIDs))
		bm25FieldIDSlice := unsafe.Slice(cBM25FieldIDs, numBM25Fields)
		bm25StatsLogIDSlice := unsafe.Slice(cBM25StatsLogIDs, numBM25Fields)
		for i, fieldID := range config.BM25FieldIDs {
			bm25FieldIDSlice[i] = C.int64_t(fieldID)
			bm25StatsLogIDSlice[i] = C.int64_t(config.BM25StatsLogIDs[i])
		}
		cConfig.bm25_field_ids = cBM25FieldIDs
		cConfig.bm25_stats_log_ids = cBM25StatsLogIDs
		cConfig.num_bm25_fields = C.size_t(numBM25Fields)
	} else {
		cConfig.bm25_field_ids = nil
		cConfig.bm25_stats_log_ids = nil
		cConfig.num_bm25_fields = 0
	}
	cConfig.write_merged_bm25_stats = C.bool(config.WriteMergedBM25Stats)
	if len(config.PKStatsBlob) > 0 {
		cPKStatsBlob := C.CBytes(config.PKStatsBlob)
		defer C.free(cPKStatsBlob)
		cConfig.pk_stats_field_id = C.int64_t(config.PKStatsFieldID)
		cConfig.pk_stats_log_id = C.int64_t(config.PKStatsLogID)
		cConfig.pk_stats_blob = (*C.uint8_t)(cPKStatsBlob)
		cConfig.pk_stats_blob_size = C.size_t(len(config.PKStatsBlob))
	}
	if len(config.MergedPKStatsBlob) > 0 {
		cMergedPKStatsBlob := C.CBytes(config.MergedPKStatsBlob)
		defer C.free(cMergedPKStatsBlob)
		cConfig.merged_pk_stats_blob = (*C.uint8_t)(cMergedPKStatsBlob)
		cConfig.merged_pk_stats_blob_size = C.size_t(len(config.MergedPKStatsBlob))
	}

	// call C FFI
	var cResult C.CFlushResult
	status := C.FlushGrowingSegmentData(
		s.ptr,
		C.int64_t(startOffset),
		C.int64_t(endOffset),
		&cConfig,
		&cResult,
	)
	defer C.FreeFlushResult(&cResult)

	if err := HandleCStatus(ctx, &status, "FlushGrowingSegmentData"); err != nil {
		return nil, err
	}

	// no data flushed
	if cResult.manifest_path == nil {
		return nil, nil
	}

	// C++ SegmentWriter returns a raw file path like:
	//   "files/insert_log/{coll}/{part}/{seg}/_metadata/manifest-{ver}.avro"
	// But GetLoonManifest() expects a JSON-encoded manifest path like:
	//   {"ver":{ver},"base_path":"files/insert_log/{coll}/{part}/{seg}"}
	// Convert using the committed_version and base path extraction.
	rawPath := C.GoString(cResult.manifest_path)
	committedVersion := int64(cResult.committed_version)

	// Extract base path: strip "/_metadata/manifest-{ver}.avro" suffix
	basePath := rawPath
	if idx := strings.Index(rawPath, "/_metadata/"); idx >= 0 {
		basePath = rawPath[:idx]
	}
	manifestPath := packed.MarshalManifestPath(basePath, committedVersion)
	fieldNullCounts := make(map[int64]int64, int(cResult.num_field_stats))
	if cResult.num_field_stats > 0 {
		fieldIDs := unsafe.Slice(cResult.field_ids, int(cResult.num_field_stats))
		nullCounts := unsafe.Slice(cResult.field_null_counts, int(cResult.num_field_stats))
		for i := 0; i < int(cResult.num_field_stats); i++ {
			fieldID := int64(fieldIDs[i])
			fieldNullCounts[fieldID] = int64(nullCounts[i])
		}
	}
	flushedFieldIDs := make([]int64, 0, int(cResult.num_flushed_fields))
	if cResult.num_flushed_fields > 0 {
		ids := unsafe.Slice(cResult.flushed_field_ids, int(cResult.num_flushed_fields))
		for i := 0; i < int(cResult.num_flushed_fields); i++ {
			flushedFieldIDs = append(flushedFieldIDs, int64(ids[i]))
		}
	}
	columnGroupMemorySizes := make(map[int64]int64, int(cResult.num_column_groups))
	if cResult.num_column_groups > 0 {
		columnGroupIDs := unsafe.Slice(cResult.column_group_ids, int(cResult.num_column_groups))
		memorySizes := unsafe.Slice(cResult.column_group_memory_sizes, int(cResult.num_column_groups))
		for i := 0; i < int(cResult.num_column_groups); i++ {
			columnGroupMemorySizes[int64(columnGroupIDs[i])] = int64(memorySizes[i])
		}
	}
	bm25Stats := make(map[int64]*storage.BM25Stats, int(cResult.num_bm25_stats))
	if cResult.num_bm25_stats > 0 {
		fieldIDs := unsafe.Slice(cResult.bm25_field_ids, int(cResult.num_bm25_stats))
		statsBytes := unsafe.Slice(cResult.bm25_stats, int(cResult.num_bm25_stats))
		statsSizes := unsafe.Slice(cResult.bm25_stats_sizes, int(cResult.num_bm25_stats))
		for i := 0; i < int(cResult.num_bm25_stats); i++ {
			bytes := C.GoBytes(unsafe.Pointer(statsBytes[i]), C.int(statsSizes[i]))
			stats, err := storage.NewBM25StatsWithBytes(bytes)
			if err != nil {
				return nil, err
			}
			bm25Stats[int64(fieldIDs[i])] = stats
		}
	}
	return &FlushResult{
		ManifestPath:           manifestPath,
		NumRows:                int64(cResult.num_rows),
		TimestampFrom:          uint64(cResult.timestamp_from),
		TimestampTo:            uint64(cResult.timestamp_to),
		FlushedFieldIDs:        flushedFieldIDs,
		ColumnGroupMemorySizes: columnGroupMemorySizes,
		FieldNullCounts:        fieldNullCounts,
		BM25Stats:              bm25Stats,
	}, nil
}
