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

import (
	"github.com/cockroachdb/errors"
	"github.com/samber/lo"
)

const (
	CanceledCode int32 = 10000
	TimeoutCode  int32 = 10001
)

type ErrorType int32

const (
	SystemError ErrorType = 0
	InputError  ErrorType = 1
)

var ErrorTypeName = map[ErrorType]string{
	SystemError: "system_error",
	InputError:  "input_error",
}

func (err ErrorType) String() string {
	return ErrorTypeName[err]
}

// Define leaf errors here,
// WARN: take care to add new error,
// check whether you can use the errors below before adding a new one.
// Name: Err + related prefix + error name
var (
	// Service related
	ErrServiceNotReady             = newMilvusError("service not ready", 1, true) // This indicates the service is still in init
	ErrServiceUnavailable          = newMilvusError("service unavailable", 2, true)
	ErrServiceMemoryLimitExceeded  = newMilvusError("memory limit exceeded", 3, false)
	ErrServiceTooManyRequests      = newMilvusError("too many concurrent requests, queue is full", 4, true)
	ErrServiceInternal             = newMilvusError("service internal error", 5, false) // Never return this error out of Milvus
	ErrServiceCrossClusterRouting  = newMilvusError("cross cluster routing", 6, false)
	ErrServiceDiskLimitExceeded    = newMilvusError("disk limit exceeded", 7, false)
	ErrServiceRateLimit            = newMilvusError("rate limit exceeded", 8, true)
	ErrServiceQuotaExceeded        = newMilvusError("quota exceeded", 9, false)
	ErrServiceUnimplemented        = newMilvusError("service unimplemented", 10, false)
	ErrServiceTimeTickLongDelay    = newMilvusError("time tick long delay", 11, false)
	ErrServiceResourceInsufficient = newMilvusError("service resource insufficient", 12, true)

	// Collection related
	ErrCollectionNotFound                      = newMilvusError("collection not found", 100, false)
	ErrCollectionNotLoaded                     = newMilvusError("collection not loaded", 101, false)
	ErrCollectionNumLimitExceeded              = newMilvusError("exceeded the limit number of collections", 102, false)
	ErrCollectionNotFullyLoaded                = newMilvusError("collection not fully loaded", 103, true)
	ErrCollectionLoaded                        = newMilvusError("collection already loaded", 104, false)
	ErrCollectionIllegalSchema                 = newMilvusError("illegal collection schema", 105, false)
	ErrCollectionOnRecovering                  = newMilvusError("collection on recovering", 106, true)
	ErrCollectionVectorClusteringKeyNotAllowed = newMilvusError("vector clustering key not allowed", 107, false)
	ErrCollectionSchemaMismatch                = newMilvusError("collection schema mismatch", 109, false)
	// Partition related
	ErrPartitionNotFound       = newMilvusError("partition not found", 200, false)
	ErrPartitionNotLoaded      = newMilvusError("partition not loaded", 201, false)
	ErrPartitionNotFullyLoaded = newMilvusError("partition not fully loaded", 202, true)

	// General capacity related
	ErrGeneralCapacityExceeded = newMilvusError("general capacity exceeded", 250, false)

	// ResourceGroup related
	ErrResourceGroupNotFound      = newMilvusError("resource group not found", 300, false)
	ErrResourceGroupAlreadyExist  = newMilvusError("resource group already exist, but create with different config", 301, false)
	ErrResourceGroupReachLimit    = newMilvusError("resource group num reach limit", 302, false)
	ErrResourceGroupIllegalConfig = newMilvusError("resource group illegal config", 303, false)
	// go:deprecated
	ErrResourceGroupNodeNotEnough    = newMilvusError("resource group node not enough", 304, false)
	ErrResourceGroupServiceAvailable = newMilvusError("resource group service available", 305, true)

	// Replica related
	ErrReplicaNotFound     = newMilvusError("replica not found", 400, false)
	ErrReplicaNotAvailable = newMilvusError("replica not available", 401, false)

	// Channel & Delegator related
	ErrChannelNotFound         = newMilvusError("channel not found", 500, false)
	ErrChannelLack             = newMilvusError("channel lacks", 501, false)
	ErrChannelReduplicate      = newMilvusError("channel reduplicates", 502, false)
	ErrChannelNotAvailable     = newMilvusError("channel not available", 503, false)
	ErrChannelCPExceededMaxLag = newMilvusError("channel checkpoint exceed max lag", 504, false)

	// Segment related
	ErrSegmentNotFound    = newMilvusError("segment not found", 600, false)
	ErrSegmentNotLoaded   = newMilvusError("segment not loaded", 601, false)
	ErrSegmentLack        = newMilvusError("segment lacks", 602, false)
	ErrSegmentReduplicate = newMilvusError("segment reduplicates", 603, false)
	ErrSegmentLoadFailed  = newMilvusError("segment load failed", 604, false)

	// Index related
	ErrIndexNotFound     = newMilvusError("index not found", 700, false)
	ErrIndexNotSupported = newMilvusError("index type not supported", 701, false)
	ErrIndexDuplicate    = newMilvusError("index duplicates", 702, false)

	// Database related
	ErrDatabaseNotFound         = newMilvusError("database not found", 800, false)
	ErrDatabaseNumLimitExceeded = newMilvusError("exceeded the limit number of database", 801, false)
	ErrDatabaseInvalidName      = newMilvusError("invalid database name", 802, false)

	// Node related
	ErrNodeNotFound        = newMilvusError("node not found", 901, false)
	ErrNodeOffline         = newMilvusError("node offline", 902, false)
	ErrNodeLack            = newMilvusError("node lacks", 903, false)
	ErrNodeNotMatch        = newMilvusError("node not match", 904, false)
	ErrNodeNotAvailable    = newMilvusError("node not available", 905, false)
	ErrNodeStateUnexpected = newMilvusError("node state unexpected", 906, false)

	// IO related
	ErrIoKeyNotFound = newMilvusError("key not found", 1000, false)
	ErrIoFailed      = newMilvusError("IO failed", 1001, false)
	ErrIoUnexpectEOF = newMilvusError("unexpected EOF", 1002, true)

	// Parameter related
	ErrParameterInvalid  = newMilvusError("invalid parameter", 1100, false)
	ErrParameterMissing  = newMilvusError("missing parameter", 1101, false)
	ErrParameterTooLarge = newMilvusError("parameter too large", 1102, false)

	// Metrics related
	ErrMetricNotFound = newMilvusError("metric not found", 1200, false)

	// Message queue related
	ErrMqTopicNotFound  = newMilvusError("topic not found", 1300, false)
	ErrMqTopicNotEmpty  = newMilvusError("topic not empty", 1301, false)
	ErrMqInternal       = newMilvusError("message queue internal error", 1302, false)
	ErrDenyProduceMsg   = newMilvusError("deny to write the message to mq", 1303, false)
	ErrTooManyConsumers = newMilvusError("consumer number limit exceeded", 1304, false)

	// Privilege related
	// this operation is denied because the user not authorized, user need to login in first
	ErrPrivilegeNotAuthenticated = newMilvusError("not authenticated", 1400, false)
	// this operation is denied because the user has no permission to do this, user need higher privilege
	ErrPrivilegeNotPermitted     = newMilvusError("privilege not permitted", 1401, false)
	ErrPrivilegeGroupInvalidName = newMilvusError("invalid privilege group name", 1402, false)

	// Alias related
	ErrAliasNotFound               = newMilvusError("alias not found", 1600, false)
	ErrAliasCollectionNameConfilct = newMilvusError("alias and collection name conflict", 1601, false)
	ErrAliasAlreadyExist           = newMilvusError("alias already exist", 1602, false)
	ErrCollectionIDOfAliasNotFound = newMilvusError("collection id of alias not found", 1603, false)

	// field related
	ErrFieldNotFound    = newMilvusError("field not found", 1700, false)
	ErrFieldInvalidName = newMilvusError("field name invalid", 1701, false)

	// high-level restful api related
	ErrNeedAuthenticate          = newMilvusError("user hasn't authenticated", 1800, false)
	ErrIncorrectParameterFormat  = newMilvusError("can only accept json format request", 1801, false)
	ErrMissingRequiredParameters = newMilvusError("missing required parameters", 1802, false)
	ErrMarshalCollectionSchema   = newMilvusError("fail to marshal collection schema", 1803, false)
	ErrInvalidInsertData         = newMilvusError("fail to deal the insert data", 1804, false)
	ErrInvalidSearchResult       = newMilvusError("fail to parse search result", 1805, false)
	ErrCheckPrimaryKey           = newMilvusError("please check the primary key and its' type can only in [int, string]", 1806, false)
	ErrHTTPRateLimit             = newMilvusError("request is rejected by limiter", 1807, true)

	// replicate related
	ErrDenyReplicateMessage = newMilvusError("deny to use the replicate message in the normal instance", 1900, false)
	ErrInvalidMsgBytes      = newMilvusError("invalid replicate msg bytes", 1901, false)
	ErrNoAssignSegmentID    = newMilvusError("no assign segment id", 1902, false)
	ErrInvalidStreamObj     = newMilvusError("invalid stream object", 1903, false)

	// Segcore related
	ErrSegcore                    = newMilvusError("segcore error", 2000, false)
	ErrSegcoreUnsupported         = newMilvusError("segcore unsupported error", 2001, false)
	ErrSegcorePretendFinished     = newMilvusError("segcore pretend finished", 2002, false)
	ErrSegcoreFollyOtherException = newMilvusError("segcore folly other exception", 2037, false) // throw from segcore.
	ErrSegcoreFollyCancel         = newMilvusError("segcore Future was canceled", 2038, false)   // throw from segcore.

	// Do NOT export this,
	// never allow programmer using this, keep only for converting unknown error to milvusError
	errUnexpected = newMilvusError("unexpected error", (1<<16)-1, false)

	// import
	ErrImportFailed = newMilvusError("importing data failed", 2100, false)

	// Search/Query related
	ErrInconsistentRequery = newMilvusError("inconsistent requery result", 2200, true)

	// Compaction
	ErrCompactionReadDeltaLogErr                  = newMilvusError("fail to read delta log", 2300, false)
	ErrIllegalCompactionPlan                      = newMilvusError("compaction plan illegal", 2301, false)
	ErrCompactionPlanConflict                     = newMilvusError("compaction plan conflict", 2302, false)
	ErrClusteringCompactionClusterNotSupport      = newMilvusError("milvus cluster not support clustering compaction", 2303, false)
	ErrClusteringCompactionCollectionNotSupport   = newMilvusError("collection not support clustering compaction", 2304, false)
	ErrClusteringCompactionCollectionIsCompacting = newMilvusError("collection is compacting", 2305, false)
	ErrClusteringCompactionNotSupportVector       = newMilvusError("vector field clustering compaction is not supported", 2306, false)
	ErrClusteringCompactionSubmitTaskFail         = newMilvusError("fail to submit task", 2307, true)
	ErrClusteringCompactionMetaError              = newMilvusError("fail to update meta in clustering compaction", 2308, true)
	ErrClusteringCompactionGetCollectionFail      = newMilvusError("fail to get collection in compaction", 2309, true)
	ErrCompactionResultNotFound                   = newMilvusError("compaction result not found", 2310, false)
	ErrAnalyzeTaskNotFound                        = newMilvusError("analyze task not found", 2311, true)
	ErrBuildCompactionRequestFail                 = newMilvusError("fail to build CompactionRequest", 2312, true)
	ErrGetCompactionPlanResultFail                = newMilvusError("fail to get compaction plan", 2313, true)
	ErrCompactionResult                           = newMilvusError("illegal compaction results", 2314, false)
	ErrDuplicatedCompactionTask                   = newMilvusError("duplicated compaction task", 2315, false)
	ErrCleanPartitionStatsFail                    = newMilvusError("fail to clean partition Stats", 2316, true)

	ErrDataNodeSlotExhausted = newMilvusError("datanode slot exhausted", 2401, false)

	// General
	ErrOperationNotSupported = newMilvusError("unsupported operation", 3000, false)
)

type errorOption func(*milvusError)

func WithDetail(detail string) errorOption {
	return func(err *milvusError) {
		err.detail = detail
	}
}

func WithErrorType(etype ErrorType) errorOption {
	return func(err *milvusError) {
		err.errType = etype
	}
}

type milvusError struct {
	msg       string
	detail    string
	retriable bool
	errCode   int32
	errType   ErrorType
}

func newMilvusError(msg string, code int32, retriable bool, options ...errorOption) milvusError {
	err := milvusError{
		msg:       msg,
		detail:    msg,
		retriable: retriable,
		errCode:   code,
	}

	for _, option := range options {
		option(&err)
	}
	return err
}

func (e milvusError) code() int32 {
	return e.errCode
}

func (e milvusError) Error() string {
	return e.msg
}

func (e milvusError) Detail() string {
	return e.detail
}

func (e milvusError) Is(err error) bool {
	cause := errors.Cause(err)
	if cause, ok := cause.(milvusError); ok {
		return e.errCode == cause.errCode
	}
	return false
}

type multiErrors struct {
	errs []error
}

func (e multiErrors) Unwrap() error {
	if len(e.errs) <= 1 {
		return nil
	}
	// To make merr work for multi errors,
	// we need cause of multi errors, which defined as the last error
	if len(e.errs) == 2 {
		return e.errs[1]
	}

	return multiErrors{
		errs: e.errs[1:],
	}
}

func (e multiErrors) Error() string {
	final := e.errs[0]
	for i := 1; i < len(e.errs); i++ {
		final = errors.Wrap(e.errs[i], final.Error())
	}
	return final.Error()
}

func (e multiErrors) Is(err error) bool {
	for _, item := range e.errs {
		if errors.Is(item, err) {
			return true
		}
	}
	return false
}

func Combine(errs ...error) error {
	errs = lo.Filter(errs, func(err error, _ int) bool { return err != nil })
	if len(errs) == 0 {
		return nil
	}
	return multiErrors{
		errs,
	}
}
