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

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
	"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
	"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
	"github.com/milvus-io/milvus/internal/coordinator/snmanager"
	"github.com/milvus-io/milvus/internal/json"
	"github.com/milvus-io/milvus/internal/storagev2/packed"
	"github.com/milvus-io/milvus/internal/streamingcoord/server/balancer"
	"github.com/milvus-io/milvus/internal/util/hookutil"
	"github.com/milvus-io/milvus/pkg/v3/common"
	"github.com/milvus-io/milvus/pkg/v3/mlog"
	"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
	"github.com/milvus-io/milvus/pkg/v3/proto/querypb"
	"github.com/milvus-io/milvus/pkg/v3/streaming/util/message"
	util "github.com/milvus-io/milvus/pkg/v3/util"
	"github.com/milvus-io/milvus/pkg/v3/util/externalspec"
	"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
	"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/retry"
	"github.com/milvus-io/milvus/pkg/v3/util/timestamptz"
	"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)

type createCollectionTask struct {
	*Core
	Req             *milvuspb.CreateCollectionRequest
	header          *message.CreateCollectionMessageHeader
	body            *message.CreateCollectionRequest
	preserveFieldID bool

	// heldFileResourceIds tracks file resources whose refCnt was incremented
	// during validation, to be released if the task fails before Broadcast.
	heldFileResourceIds []int64
}

// releaseFileResources decrements refCnt for file resources that were
// incremented during validation. Called when the task fails before Broadcast.
func (t *createCollectionTask) releaseFileResources() {
	if len(t.heldFileResourceIds) > 0 {
		t.meta.DecFileResourceRefCnt(t.heldFileResourceIds)
		t.heldFileResourceIds = nil
	}
}

func (t *createCollectionTask) validate(ctx context.Context) error {
	if t.Req == nil {
		return merr.WrapErrServiceInternalMsg("empty requests")
	}
	Params := paramtable.Get()

	// 1. check shard number
	shardsNum := t.Req.GetShardsNum()
	var cfgMaxShardNum int32
	if Params.CommonCfg.PreCreatedTopicEnabled.GetAsBool() {
		cfgMaxShardNum = int32(len(Params.CommonCfg.TopicNames.GetAsStrings()))
	} else {
		cfgMaxShardNum = Params.RootCoordCfg.DmlChannelNum.GetAsInt32()
	}
	if shardsNum > cfgMaxShardNum {
		return merr.WrapErrParameterInvalidMsg("shard num (%d) exceeds max configuration (%d)", shardsNum, cfgMaxShardNum)
	}

	cfgShardLimit := Params.ProxyCfg.MaxShardNum.GetAsInt32()
	if shardsNum > cfgShardLimit {
		return merr.WrapErrParameterInvalidMsg("shard num (%d) exceeds system limit (%d)", shardsNum, cfgShardLimit)
	}

	// 2. check db-collection capacity
	db2CollIDs := t.meta.ListAllAvailCollections(ctx)
	if err := t.checkMaxCollectionsPerDB(ctx, db2CollIDs); err != nil {
		return err
	}

	// 3. check total collection number
	totalCollections := 0
	for _, collIDs := range db2CollIDs {
		totalCollections += len(collIDs)
	}

	maxCollectionNum := Params.QuotaConfig.MaxCollectionNum.GetAsInt()
	if totalCollections >= maxCollectionNum {
		mlog.Warn(ctx, "unable to create collection because the number of collection has reached the limit", mlog.Int("max_collection_num", maxCollectionNum))
		return merr.WrapErrCollectionNumLimitExceeded(t.Req.GetDbName(), maxCollectionNum)
	}

	// 4. check collection * shard * partition
	var newPartNum int64 = 1
	if t.Req.GetNumPartitions() > 0 {
		newPartNum = t.Req.GetNumPartitions()
	}
	return checkGeneralCapacity(ctx, 1, newPartNum, t.Req.GetShardsNum(), t.Core)
}

// checkMaxCollectionsPerDB DB properties take precedence over quota configurations for max collections.
func (t *createCollectionTask) checkMaxCollectionsPerDB(ctx context.Context, db2CollIDs map[int64][]int64) error {
	Params := paramtable.Get()

	collIDs, ok := db2CollIDs[t.header.DbId]
	if !ok {
		mlog.Warn(ctx, "can not found DB ID", mlog.String("collection", t.Req.GetCollectionName()), mlog.String("dbName", t.Req.GetDbName()))
		return merr.WrapErrDatabaseNotFound(t.Req.GetDbName(), "failed to create collection")
	}

	db, err := t.meta.GetDatabaseByName(ctx, t.Req.GetDbName(), typeutil.MaxTimestamp)
	if err != nil {
		mlog.Warn(ctx, "can not found DB ID", mlog.String("collection", t.Req.GetCollectionName()), mlog.String("dbName", t.Req.GetDbName()))
		return merr.WrapErrDatabaseNotFound(t.Req.GetDbName(), "failed to create collection")
	}

	check := func(maxColNumPerDB int) error {
		if len(collIDs) >= maxColNumPerDB {
			mlog.Warn(ctx, "unable to create collection because the number of collection has reached the limit in DB", mlog.Int("maxCollectionNumPerDB", maxColNumPerDB))
			return merr.WrapErrCollectionNumLimitExceeded(t.Req.GetDbName(), maxColNumPerDB)
		}
		return nil
	}

	maxColNumPerDBStr := db.GetProperty(common.DatabaseMaxCollectionsKey)
	if maxColNumPerDBStr != "" {
		maxColNumPerDB, err := strconv.Atoi(maxColNumPerDBStr)
		if err != nil {
			mlog.Warn(ctx, "parse value of property fail", mlog.String("key", common.DatabaseMaxCollectionsKey),
				mlog.String("value", maxColNumPerDBStr), mlog.Err(err))
			return merr.WrapErrServiceInternalMsg("parse value of property fail, key:%s, value:%s", common.DatabaseMaxCollectionsKey, maxColNumPerDBStr)
		}
		return check(maxColNumPerDB)
	}

	maxColNumPerDB := Params.QuotaConfig.MaxCollectionNumPerDB.GetAsInt()
	return check(maxColNumPerDB)
}

func checkGeometryDefaultValue(value string) error {
	if _, err := common.ConvertWKTToWKB(value); err != nil {
		mlog.Warn(context.TODO(), "invalid default value for geometry field", mlog.Err(err))
		return merr.WrapErrParameterInvalidMsg("invalid default value for geometry field")
	}

	return nil
}

func hasSystemFields(schema *schemapb.CollectionSchema, systemFields []string) bool {
	for _, f := range schema.GetFields() {
		if funcutil.SliceContain(systemFields, f.GetName()) {
			return true
		}
	}
	return false
}

func (t *createCollectionTask) validateSchema(ctx context.Context, schema *schemapb.CollectionSchema) error {
	mlog.With(mlog.String("CollectionName", t.Req.CollectionName))
	if t.Req.GetCollectionName() != schema.GetName() {
		mlog.Error(ctx, "collection name not matches schema name", mlog.String("SchemaName", schema.Name))
		msg := fmt.Sprintf("collection name = %s, schema.Name=%s", t.Req.GetCollectionName(), schema.Name)
		return merr.WrapErrParameterInvalid("collection name matches schema name", "don't match", msg)
	}

	if err := checkFieldSchema(schema.GetFields()); err != nil {
		return err
	}

	// Validate default
	if err := timestamptz.CheckAndRewriteTimestampTzDefaultValue(schema); err != nil {
		return err
	}

	if err := checkStructArrayFieldSchema(schema.GetStructArrayFields()); err != nil {
		return err
	}

	// Note: this call mutates schema (sets nullable=true on every user field).
	// See NormalizeAndValidateExternalCollectionSchema for the rationale.
	if err := typeutil.NormalizeAndValidateExternalCollectionSchema(schema); err != nil {
		return err
	}
	if err := typeutil.ValidateTextRequiresStorageV3(schema, Params.CommonCfg.UseLoonFFI.GetAsBool()); err != nil {
		return merr.WrapErrParameterInvalidMsg("%s", err.Error())
	}

	// For external collections, validate the source URL scheme allowlist and
	// the JSON spec structure (extfs allowlist + format whitelist) at the
	// RootCoord side as well — defense in depth in case a request bypasses
	// proxy-side validation.
	// Skip validation for empty ExternalSource — IsExternalCollection allows
	// it (external fields without a concrete source), so the validator must
	// too. When a source is eventually supplied via alter/refresh, validation
	// runs there.
	if typeutil.IsExternalCollection(schema) && schema.GetExternalSource() != "" {
		if err := externalspec.ValidateSourceAndSpec(schema.GetExternalSource(), schema.GetExternalSpec()); err != nil {
			return err
		}
	}

	if hasSystemFields(schema, []string{RowIDFieldName, TimeStampFieldName, MetaFieldName, NamespaceFieldName}) {
		mlog.Error(ctx, "schema contains system field",
			mlog.String("RowIDFieldName", RowIDFieldName),
			mlog.String("TimeStampFieldName", TimeStampFieldName),
			mlog.String("MetaFieldName", MetaFieldName),
			mlog.String("NamespaceFieldName", NamespaceFieldName))
		msg := fmt.Sprintf("schema contains system field: %s, %s, %s, %s", RowIDFieldName, TimeStampFieldName, MetaFieldName, NamespaceFieldName)
		return merr.WrapErrParameterInvalid("schema don't contains system field", "contains", msg)
	}

	if err := validateStructArrayFieldDataType(schema.GetStructArrayFields()); err != nil {
		return err
	}

	fileResourceIds, err := t.validateSchemaAnalyzerFileResources(ctx, schema)
	if err != nil {
		return err
	}
	schema.FileResourceIds = fileResourceIds

	if len(schema.FileResourceIds) > 0 {
		// Bind file resources to collection lifecycle: refCnt++ now, refCnt-- on
		// drop. Under ddLock, atomic with RemoveFileResource. See #48612.
		if err := reserveFileResourceRefs(t.meta, schema.FileResourceIds); err != nil {
			return err
		}
		t.heldFileResourceIds = schema.FileResourceIds
	}

	return validateFieldDataType(schema.GetFields())
}

func (c *Core) validateSchemaAnalyzerFileResources(ctx context.Context, schema *schemapb.CollectionSchema) ([]int64, error) {
	analyzerInfos, err := collectAnalyzerInfos(schema)
	if err != nil {
		return nil, err
	}
	return c.validateAnalyzerInfos(ctx, analyzerInfos)
}

func (c *Core) validateAnalyzerInfos(ctx context.Context, analyzerInfos []*querypb.AnalyzerInfo) ([]int64, error) {
	if len(analyzerInfos) == 0 {
		return nil, nil
	}

	// validate analyzer params at any streaming node
	// and set file resource ids to schema
	err := retry.Do(ctx, func() error {
		if c.fileResourceObserver == nil {
			return nil
		}
		if err := c.fileResourceObserver.CheckAllQnReady(); err != nil {
			return err
		}
		return nil
	}, retry.Attempts(10), retry.Sleep(3*time.Second))
	if err != nil {
		return nil, err
	}

	resp, err := c.mixCoord.ValidateAnalyzer(ctx, &querypb.ValidateAnalyzerRequest{
		AnalyzerInfos: analyzerInfos,
	})
	if err != nil {
		return nil, err
	}

	if err := merr.Error(resp.GetStatus()); err != nil {
		return nil, err
	}
	return resp.GetResourceIds(), nil
}

func (t *createCollectionTask) assignFieldAndFunctionID(schema *schemapb.CollectionSchema) error {
	idx := 0
	for _, field := range schema.GetFields() {
		field.FieldID = int64(idx + StartOfUserFieldID)
		idx++
	}

	for _, structArrayField := range schema.GetStructArrayFields() {
		structArrayField.FieldID = int64(idx + StartOfUserFieldID)
		idx++

		for _, field := range structArrayField.GetFields() {
			field.FieldID = int64(idx + StartOfUserFieldID)
			idx++
		}
	}

	return assignFunctionIDsFromFieldNames(schema)
}

// assignFunctionIDsFromFieldNames resolves function input/output field IDs
// after field IDs have been assigned or aligned from a source snapshot.
func assignFunctionIDsFromFieldNames(schema *schemapb.CollectionSchema) error {
	name2id := map[string]int64{}
	for _, field := range schema.GetFields() {
		name2id[field.GetName()] = field.GetFieldID()
	}
	for _, structArrayField := range schema.GetStructArrayFields() {
		for _, field := range structArrayField.GetFields() {
			name2id[field.GetName()] = field.GetFieldID()
		}
	}

	for fidx, function := range schema.GetFunctions() {
		function.InputFieldIds = make([]int64, len(function.InputFieldNames))
		function.Id = int64(fidx) + StartOfUserFunctionID
		for idx, name := range function.InputFieldNames {
			fieldId, ok := name2id[name]
			if !ok {
				return merr.WrapErrParameterInvalidMsg("input field %s of function %s not found", name, function.GetName())
			}
			function.InputFieldIds[idx] = fieldId
		}

		function.OutputFieldIds = make([]int64, len(function.OutputFieldNames))
		for idx, name := range function.OutputFieldNames {
			fieldId, ok := name2id[name]
			if !ok {
				return merr.WrapErrParameterInvalidMsg("output field %s of function %s not found", name, function.GetName())
			}
			function.OutputFieldIds[idx] = fieldId
		}
	}

	return nil
}

func (t *createCollectionTask) appendDynamicField(ctx context.Context, schema *schemapb.CollectionSchema) {
	if schema.EnableDynamicField {
		schema.Fields = append(schema.Fields, &schemapb.FieldSchema{
			Name:        MetaFieldName,
			Description: "dynamic schema",
			DataType:    schemapb.DataType_JSON,
			IsDynamic:   true,
			Nullable:    true,
			DefaultValue: &schemapb.ValueField{
				Data: &schemapb.ValueField_BytesData{
					BytesData: []byte("{}"),
				},
			},
		})
		mlog.Info(ctx, "append dynamic field", mlog.String("collection", schema.Name))
	}
}

func (t *createCollectionTask) appendConsistecyLevel() {
	if ok, _ := getConsistencyLevel(t.Req.Properties...); ok {
		return
	}
	for _, p := range t.Req.Properties {
		if p.GetKey() == common.ConsistencyLevel {
			// if there's already a consistency level, overwrite it.
			p.Value = strconv.Itoa(int(t.Req.ConsistencyLevel))
			return
		}
	}
	// append consistency level into schema properties
	t.Req.Properties = append(t.Req.Properties, &commonpb.KeyValuePair{
		Key:   common.ConsistencyLevel,
		Value: strconv.Itoa(int(t.Req.ConsistencyLevel)),
	})
}

func (t *createCollectionTask) appendNamespaceShardingEnabled() error {
	if err := common.ValidateNamespaceShardingEnabled(t.Req.Properties...); err != nil {
		return err
	}
	if common.IsNamespaceShardingEnabledKeyExists(t.Req.Properties...) {
		return nil
	}
	t.Req.Properties = append(t.Req.Properties, &commonpb.KeyValuePair{
		Key:   common.NamespaceShardingEnabledKey,
		Value: "false",
	})
	return nil
}

func (t *createCollectionTask) handleNamespaceField(ctx context.Context, schema *schemapb.CollectionSchema) error {
	hasIsolation := hasIsolationProperty(t.Req.Properties...)
	_, err := typeutil.GetPartitionKeyFieldSchema(schema)
	hasPartitionKey := err == nil
	if !schema.GetEnableNamespace() {
		return nil
	}

	if common.IsNamespaceModePartition(t.Req.GetProperties()...) {
		if hasPartitionKey {
			return merr.WrapErrParameterInvalidMsg("namespace is not supported with partition key mode")
		}
		return nil
	}

	if typeutil.IsExternalCollection(schema) {
		return merr.WrapErrParameterInvalidMsg("external collection does not support namespace field")
	}

	if hasIsolation {
		iso, err := common.IsPartitionKeyIsolationKvEnabled(t.Req.Properties...)
		if err != nil {
			return err
		}
		if !iso {
			return merr.WrapErrCollectionIllegalSchema(t.Req.CollectionName,
				"isolation property is false when namespace enabled")
		}
	}

	if hasPartitionKey {
		return merr.WrapErrParameterInvalidMsg("namespace is not supported with partition key mode")
	}

	if common.IsNamespaceModePartition(t.Req.GetProperties()...) {
		return nil
	}

	schema.Fields = append(schema.Fields, &schemapb.FieldSchema{
		Name:           common.NamespaceFieldName,
		IsPartitionKey: true,
		DataType:       schemapb.DataType_VarChar,
		TypeParams: []*commonpb.KeyValuePair{
			{Key: common.MaxLengthKey, Value: fmt.Sprintf("%d", paramtable.Get().ProxyCfg.MaxVarCharLength.GetAsInt())},
		},
	})
	t.Req.Properties = append(t.Req.Properties, &commonpb.KeyValuePair{
		Key:   common.PartitionKeyIsolationKey,
		Value: "true",
	})
	mlog.Info(ctx, "added namespace field",
		mlog.String("collectionName", t.Req.CollectionName),
		mlog.String("fieldName", common.NamespaceFieldName))
	return nil
}

func hasIsolationProperty(props ...*commonpb.KeyValuePair) bool {
	for _, p := range props {
		if p.GetKey() == common.PartitionKeyIsolationKey {
			return true
		}
	}
	return false
}

func (t *createCollectionTask) appendSysFields(schema *schemapb.CollectionSchema) {
	schema.Fields = append(schema.Fields, &schemapb.FieldSchema{
		FieldID:      int64(RowIDField),
		Name:         RowIDFieldName,
		IsPrimaryKey: false,
		Description:  "row id",
		DataType:     schemapb.DataType_Int64,
	})
	schema.Fields = append(schema.Fields, &schemapb.FieldSchema{
		FieldID:      int64(TimeStampField),
		Name:         TimeStampFieldName,
		IsPrimaryKey: false,
		Description:  "time stamp",
		DataType:     schemapb.DataType_Int64,
	})
}

// prepareMilvusTableSnapshotSchema validates the source snapshot and aligns
// target field IDs before normal create-collection field ID assignment runs.
func (t *createCollectionTask) prepareMilvusTableSnapshotSchema(ctx context.Context) error {
	schema := t.body.CollectionSchema
	if schema == nil || schema.GetExternalSource() == "" || schema.GetExternalSpec() == "" {
		return nil
	}
	// Validate before reading snapshot metadata so RootCoord keeps the same
	// external source boundary even if a request bypasses Proxy.
	if err := externalspec.ValidateSourceAndSpec(schema.GetExternalSource(), schema.GetExternalSpec()); err != nil {
		return err
	}
	spec, err := externalspec.ParseExternalSpec(schema.GetExternalSpec())
	if err != nil {
		return err
	}
	if spec.Format != externalspec.FormatMilvusTable {
		return nil
	}
	if t.preserveFieldID {
		// DDL replay carries the schema after milvus-table field-ID alignment.
		// Re-reading the source snapshot here would make RootCoord recovery
		// depend on the external bucket and credentials still being available.
		return nil
	}

	metadata, err := packed.ReadMilvusTableSnapshotMetadata(
		schema.GetExternalSource(),
		schema.GetExternalSpec(),
		createMilvusTableSnapshotStorageConfig(),
		packed.ExternalSpecContext{
			Source: schema.GetExternalSource(),
			Spec:   schema.GetExternalSpec(),
		},
	)
	if err != nil {
		return merr.Wrap(err, "read milvus-table snapshot metadata for schema alignment")
	}
	sourceSchema := metadata.GetCollection().GetSchema()
	if sourceSchema == nil {
		return merr.WrapErrParameterInvalidMsg("milvus-table snapshot metadata missing collection schema")
	}
	if typeutil.IsExternalCollection(sourceSchema) {
		// Avoid external-table chaining. A chained source would require refresh
		// and read paths to chase another collection's external source/storage
		// contract, which is not part of the milvus-table snapshot contract.
		return merr.WrapErrParameterInvalidMsg("milvus-table external collection cannot use an external collection snapshot as source")
	}
	if err := typeutil.ValidateMilvusTableSchemaIdentity(schema, sourceSchema, false); err != nil {
		return merr.Wrap(err, "milvus-table target schema must match source snapshot schema")
	}

	sourceFields := milvusTableSourceFieldsByName(sourceSchema)
	nextTargetOnlyFieldID := nextMilvusTableTargetOnlyFieldID(sourceSchema)
	for _, field := range schema.GetFields() {
		if field.GetName() == common.VirtualPKFieldName || typeutil.IsFunctionOutputField(schema, field) {
			// Milvus-table mapped fields must reuse source field IDs because
			// source manifests store physical columns by field ID. Target-only
			// fields, including virtual PK and target function outputs, are not
			// read from the source manifest and therefore need IDs outside the
			// source snapshot range.
			field.FieldID = nextTargetOnlyFieldID
			nextTargetOnlyFieldID++
			continue
		}
		if typeutil.IsExternalSystemOrVirtualField(field.GetName()) {
			continue
		}
		sourceField := sourceFields[field.GetExternalField()]
		if sourceField == nil {
			return merr.WrapErrParameterInvalidMsg("milvus-table target field %q maps to missing source field %q", field.GetName(), field.GetExternalField())
		}
		field.FieldID = sourceField.GetFieldID()
	}
	if err := assignFunctionIDsFromFieldNames(schema); err != nil {
		return merr.Wrap(err, "align milvus-table function field IDs")
	}
	t.preserveFieldID = true
	t.Req.Properties = upsertCreateCollectionProperty(t.Req.GetProperties(), util.PreserveFieldIdsKey, "true")

	mlog.Info(ctx, "aligned milvus-table external collection field IDs with source snapshot",
		mlog.String("collection", t.Req.GetCollectionName()),
		mlog.String("externalSource", schema.GetExternalSource()))
	return nil
}

// createMilvusTableSnapshotStorageConfig builds the local Milvus storage config
// used to read source snapshot metadata during create collection.
func createMilvusTableSnapshotStorageConfig() *indexpb.StorageConfig {
	params := paramtable.Get()
	if params.CommonCfg.StorageType.GetValue() == "local" {
		return &indexpb.StorageConfig{
			RootPath:    params.LocalStorageCfg.Path.GetValue(),
			StorageType: params.CommonCfg.StorageType.GetValue(),
		}
	}
	return &indexpb.StorageConfig{
		Address:           params.MinioCfg.Address.GetValue(),
		AccessKeyID:       params.MinioCfg.AccessKeyID.GetValue(),
		SecretAccessKey:   params.MinioCfg.SecretAccessKey.GetValue(),
		UseSSL:            params.MinioCfg.UseSSL.GetAsBool(),
		SslCACert:         params.MinioCfg.SslCACert.GetValue(),
		BucketName:        params.MinioCfg.BucketName.GetValue(),
		RootPath:          params.MinioCfg.RootPath.GetValue(),
		UseIAM:            params.MinioCfg.UseIAM.GetAsBool(),
		IAMEndpoint:       params.MinioCfg.IAMEndpoint.GetValue(),
		StorageType:       params.CommonCfg.StorageType.GetValue(),
		Region:            params.MinioCfg.Region.GetValue(),
		UseVirtualHost:    params.MinioCfg.UseVirtualHost.GetAsBool(),
		CloudProvider:     params.MinioCfg.CloudProvider.GetValue(),
		RequestTimeoutMs:  params.MinioCfg.RequestTimeoutMs.GetAsInt64(),
		GcpCredentialJSON: params.MinioCfg.GcpCredentialJSON.GetValue(),
		SslTlsMinVersion:  params.MinioCfg.SslTLSMinVersion.GetValue(),
		UseCrc32CChecksum: params.MinioCfg.UseCRC32C.GetAsBool(),
	}
}

// nextMilvusTableTargetOnlyFieldID returns the first field ID above all source
// snapshot IDs so target-only fields cannot collide with source columns.
func nextMilvusTableTargetOnlyFieldID(schemas ...*schemapb.CollectionSchema) int64 {
	next := int64(StartOfUserFieldID)
	for _, schema := range schemas {
		for _, field := range schema.GetFields() {
			if field.GetFieldID() >= next {
				next = field.GetFieldID() + 1
			}
		}
	}
	return next
}

// milvusTableSourceFieldsByName indexes source user fields that can be targets
// of ExternalField mappings.
func milvusTableSourceFieldsByName(schema *schemapb.CollectionSchema) map[string]*schemapb.FieldSchema {
	fields := make(map[string]*schemapb.FieldSchema, len(schema.GetFields()))
	for _, field := range schema.GetFields() {
		if typeutil.IsExternalSystemOrVirtualField(field.GetName()) {
			continue
		}
		fields[field.GetName()] = field
	}
	return fields
}

// upsertCreateCollectionProperty inserts or updates one create-collection
// property without disturbing the remaining request properties.
func upsertCreateCollectionProperty(properties []*commonpb.KeyValuePair, key, value string) []*commonpb.KeyValuePair {
	for _, property := range properties {
		if property.GetKey() == key {
			property.Value = value
			return properties
		}
	}
	return append(properties, &commonpb.KeyValuePair{Key: key, Value: value})
}

func (t *createCollectionTask) prepareSchema(ctx context.Context) error {
	if err := t.prepareMilvusTableSnapshotSchema(ctx); err != nil {
		return err
	}

	// if schema comes from restore snapshot
	preservedDynamicFieldID := int64(-1)
	preservedNamespaceFieldID := int64(-1)
	if t.preserveFieldID {
		mlog.Info(ctx, "preserve field IDs from schema during create collection", mlog.String("collection", t.Req.CollectionName))
		fields := make([]*schemapb.FieldSchema, 0)
		// filter out system fields
		for _, field := range t.body.CollectionSchema.Fields {
			if field.Name != RowIDFieldName && field.GetFieldID() == 0 {
				mlog.Info(context.TODO(), "field id 0 is not allowed when preserve field ids", mlog.String("field", field.Name))
				return merr.WrapErrParameterInvalidMsg("field id 0 is not allowed when preserve field ids, field: %s", field.Name)
			}

			if field.GetName() == MetaFieldName {
				preservedDynamicFieldID = field.GetFieldID()
				continue
			}
			if field.GetName() == NamespaceFieldName {
				preservedNamespaceFieldID = field.GetFieldID()
				continue
			}
			if field.GetName() == TimeStampFieldName || field.GetName() == RowIDFieldName {
				continue
			}
			fields = append(fields, field)
		}
		t.body.CollectionSchema.Fields = fields
	}

	if err := t.validateSchema(ctx, t.body.CollectionSchema); err != nil {
		return err
	}

	t.appendConsistecyLevel()
	if err := t.appendNamespaceShardingEnabled(); err != nil {
		return err
	}
	t.appendDynamicField(ctx, t.body.CollectionSchema)
	if err := common.ValidateNamespaceMode(t.Req.GetProperties()...); err != nil {
		return err
	}
	if err := t.handleNamespaceField(ctx, t.body.CollectionSchema); err != nil {
		return err
	}

	if t.preserveFieldID {
		// cause dynamic field is system field without internal id allocation
		// we need to restore its field id here
		for _, field := range t.body.CollectionSchema.Fields {
			if field.GetName() == MetaFieldName {
				field.FieldID = preservedDynamicFieldID
			}

			if field.GetName() == NamespaceFieldName {
				field.FieldID = preservedNamespaceFieldID
			}
		}
	} else {
		if err := t.assignFieldAndFunctionID(t.body.CollectionSchema); err != nil {
			return err
		}
	}
	if err := typeutil.ValidateExternalCollectionResolvedSchema(t.body.CollectionSchema); err != nil {
		return err
	}

	// Validate timezone
	tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, t.Req.GetProperties())
	if exist && !timestamptz.IsTimezoneValid(tz) {
		return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
	}

	// Set properties for persistent
	t.body.CollectionSchema.Properties = updateMaxFieldIDProperty(t.Req.GetProperties(), maxAssignedFieldIDFromSchema(t.body.CollectionSchema))
	t.body.CollectionSchema.Version = 0
	t.appendSysFields(t.body.CollectionSchema)
	return nil
}

func (t *createCollectionTask) assignCollectionID() error {
	var err error
	t.header.CollectionId, err = t.idAllocator.AllocOne()
	t.body.CollectionID = t.header.CollectionId
	return err
}

func (t *createCollectionTask) assignPartitionIDs(ctx context.Context) error {
	Params := paramtable.Get()

	partitionNames := make([]string, 0, t.Req.GetNumPartitions())
	defaultPartitionName := Params.CommonCfg.DefaultPartitionName.GetValue()

	if _, err := typeutil.GetPartitionKeyFieldSchema(t.body.CollectionSchema); err == nil {
		// only when enabling partition key mode, we allow to create multiple partitions.
		partitionNums := t.Req.GetNumPartitions()
		// double check, default num of physical partitions should be greater than 0
		if partitionNums <= 0 {
			return merr.WrapErrParameterInvalidMsg("the specified partitions should be greater than 0 if partition key is used")
		}

		cfgMaxPartitionNum := Params.RootCoordCfg.MaxPartitionNum.GetAsInt64()
		if partitionNums > cfgMaxPartitionNum {
			return merr.WrapErrParameterInvalidMsg("partition number (%d) exceeds max configuration (%d), collection: %s",
				partitionNums, cfgMaxPartitionNum, t.Req.CollectionName)
		}

		for i := int64(0); i < partitionNums; i++ {
			partitionNames = append(partitionNames, fmt.Sprintf("%s_%d", defaultPartitionName, i))
		}
	} else {
		// compatible with old versions <= 2.2.8
		partitionNames = append(partitionNames, defaultPartitionName)
	}

	// allocate partition ids
	start, end, err := t.idAllocator.Alloc(uint32(len(partitionNames)))
	if err != nil {
		return err
	}
	t.header.PartitionIds = make([]int64, len(partitionNames))
	t.body.PartitionIDs = make([]int64, len(partitionNames))
	for i := start; i < end; i++ {
		t.header.PartitionIds[i-start] = i
		t.body.PartitionIDs[i-start] = i
	}
	t.body.PartitionNames = partitionNames

	mlog.Info(ctx, "assign partitions when create collection",
		mlog.String("collectionName", t.Req.GetCollectionName()),
		mlog.Int64s("partitionIds", t.header.PartitionIds),
		mlog.Strings("partitionNames", t.body.PartitionNames))
	return nil
}

func (t *createCollectionTask) assignChannels(ctx context.Context) error {
	vchannels, err := snmanager.StaticStreamingNodeManager.AllocVirtualChannels(ctx, balancer.AllocVChannelParam{
		CollectionID: t.header.GetCollectionId(),
		Num:          int(t.Req.GetShardsNum()),
	})
	if err != nil {
		return merr.Wrapf(err, "failed to allocate vchannels for collection %d (shards=%d)",
			t.header.GetCollectionId(), t.Req.GetShardsNum())
	}

	for _, vchannel := range vchannels {
		t.body.PhysicalChannelNames = append(t.body.PhysicalChannelNames, funcutil.ToPhysicalChannel(vchannel))
		t.body.VirtualChannelNames = append(t.body.VirtualChannelNames, vchannel)
	}
	return nil
}

func (t *createCollectionTask) Prepare(ctx context.Context) error {
	t.body.Base = &commonpb.MsgBase{
		MsgType: commonpb.MsgType_CreateCollection,
	}

	db, err := t.meta.GetDatabaseByName(ctx, t.Req.GetDbName(), typeutil.MaxTimestamp)
	if err != nil {
		return err
	}
	// set collection timezone
	properties := t.Req.GetProperties()
	_, ok := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, properties)
	if !ok {
		dbTz, ok2 := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, db.Properties)
		if !ok2 {
			dbTz = common.DefaultTimezone
		}
		timezoneKV := &commonpb.KeyValuePair{Key: common.TimezoneKey, Value: dbTz}
		t.Req.Properties = append(properties, timezoneKV)
	}

	t.Req.Properties = hookutil.TidyCollPropsByDBProps(t.Req.Properties, db.Properties)

	t.header.DbId = db.ID
	t.body.DbID = t.header.DbId
	if err := t.validate(ctx); err != nil {
		return err
	}

	if err := t.prepareSchema(ctx); err != nil {
		return err
	}

	if err := t.assignCollectionID(); err != nil {
		return err
	}

	if err := t.assignPartitionIDs(ctx); err != nil {
		return err
	}

	if err := t.assignChannels(ctx); err != nil {
		return err
	}

	return t.validateIfCollectionExists(ctx)
}

func (t *createCollectionTask) validateIfCollectionExists(ctx context.Context) error {
	// Check if the collection name duplicates an alias.
	if _, err := t.meta.DescribeAlias(ctx, t.Req.GetDbName(), t.Req.GetCollectionName(), typeutil.MaxTimestamp); err == nil {
		err2 := merr.WrapErrAsInputError(merr.WrapErrAliasCollectionNameConflict(t.Req.GetDbName(), t.Req.GetCollectionName(), "please choose a unique name"))
		mlog.Warn(ctx, "create collection failed", mlog.String("database", t.Req.GetDbName()), mlog.Err(err2))
		return err2
	}

	// Check if the collection already exists.
	existedCollInfo, err := t.meta.GetCollectionByName(ctx, t.Req.GetDbName(), t.Req.GetCollectionName(), typeutil.MaxTimestamp, false)
	if err == nil {
		newCollInfo := newCollectionModel(t.header, t.body, 0)
		if equal := existedCollInfo.Equal(*newCollInfo); !equal {
			return merr.WrapErrParameterInvalidMsg("create duplicate collection with different parameters, collection: %s", t.Req.GetCollectionName())
		}
		return errIgnoredCreateCollection
	}
	return nil
}

func validateMultiAnalyzerParams(params string, coll *schemapb.CollectionSchema, fieldSchema *schemapb.FieldSchema, infos *[]*querypb.AnalyzerInfo) error {
	var m map[string]json.RawMessage
	var analyzerMap map[string]json.RawMessage
	var mFileName string

	err := json.Unmarshal([]byte(params), &m)
	if err != nil {
		return err
	}

	mfield, ok := m["by_field"]
	if !ok {
		return merr.WrapErrParameterInvalidMsg("multi analyzer params now must set by_field to specify with field decide analyzer")
	}

	err = json.Unmarshal(mfield, &mFileName)
	if err != nil {
		return merr.WrapErrParameterInvalidMsg("multi analyzer params by_field must be string but now: %s", mfield)
	}

	// check field exist
	fieldExist := false
	for _, field := range coll.GetFields() {
		if field.GetName() == mFileName {
			// only support string field now
			if field.GetDataType() != schemapb.DataType_VarChar {
				return merr.WrapErrParameterInvalidMsg("multi analyzer params now only support by string field, but field %s is not string", field.GetName())
			}
			fieldExist = true
			break
		}
	}

	if !fieldExist {
		return merr.WrapErrParameterInvalidMsg("multi analyzer dependent field %s not exist in collection %s", string(mfield), coll.GetName())
	}

	if value, ok := m["alias"]; ok {
		mapping := map[string]string{}
		err = json.Unmarshal(value, &mapping)
		if err != nil {
			return merr.WrapErrParameterInvalidMsg("multi analyzer alias must be string map but now: %s", value)
		}
	}

	analyzers, ok := m["analyzers"]
	if !ok {
		return merr.WrapErrParameterInvalidMsg("multi analyzer params must set analyzers ")
	}

	err = json.Unmarshal(analyzers, &analyzerMap)
	if err != nil {
		return merr.WrapErrParameterInvalidMsg("unmarshal analyzers failed: %s", err)
	}

	hasDefault := false
	for name, bytes := range analyzerMap {
		*infos = append(*infos, &querypb.AnalyzerInfo{
			Name:   name,
			Field:  fieldSchema.GetName(),
			Params: string(bytes),
		})
		if name == "default" {
			hasDefault = true
		}
	}

	if !hasDefault {
		return merr.WrapErrParameterInvalidMsg("multi analyzer must set default analyzer for all unknown value")
	}
	return nil
}

func collectAnalyzerInfos(collSchema *schemapb.CollectionSchema) ([]*querypb.AnalyzerInfo, error) {
	analyzerInfos := make([]*querypb.AnalyzerInfo, 0)
	for _, field := range collSchema.GetFields() {
		if err := validateAnalyzer(collSchema, field, &analyzerInfos); err != nil {
			return nil, err
		}
	}
	return analyzerInfos, nil
}

// validateAnalyzer validates active match/BM25 fields and fields with
// enable_analyzer=true. Analyzer params are ignored while analyzer is disabled.
func validateAnalyzer(collSchema *schemapb.CollectionSchema, fieldSchema *schemapb.FieldSchema, analyzerInfos *[]*querypb.AnalyzerInfo) error {
	h := typeutil.CreateFieldSchemaHelper(fieldSchema)
	active := h.EnableMatch() || typeutil.IsBm25FunctionInputField(collSchema, fieldSchema)
	if active && !h.EnableAnalyzer() {
		return merr.WrapErrParameterInvalidMsg("field %s which has enable_match or is input of BM25 function must also enable_analyzer", fieldSchema.Name)
	}
	if !h.EnableAnalyzer() {
		return nil
	}

	if params, ok := h.GetMultiAnalyzerParams(); ok {
		if h.EnableMatch() {
			return merr.WrapErrParameterInvalidMsg("multi analyzer now only support for bm25, but now field %s enable match", fieldSchema.Name)
		}
		if h.HasAnalyzerParams() {
			return merr.WrapErrParameterInvalidMsg("field %s analyzer params should be none if has multi analyzer params", fieldSchema.Name)
		}

		return validateMultiAnalyzerParams(params, collSchema, fieldSchema, analyzerInfos)
	}

	for _, kv := range fieldSchema.GetTypeParams() {
		if kv.GetKey() == "analyzer_params" {
			*analyzerInfos = append(*analyzerInfos, &querypb.AnalyzerInfo{
				Field:  fieldSchema.GetName(),
				Params: kv.GetValue(),
			})
		}
	}
	// return nil when use default analyzer
	return nil
}
