// Copyright 2023 PingCAP, Inc.
//
// Licensed 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 errctx

import (
	"github.com/pingcap/errors"
	"github.com/pingcap/tidb/pkg/errno"
	contextutil "github.com/pingcap/tidb/pkg/util/context"
	"github.com/pingcap/tidb/pkg/util/intest"
)

// Level defines the behavior for each error
type Level uint8

const (
	// LevelError means the error will be returned
	LevelError Level = iota
	// LevelWarn means it will be regarded as a warning
	LevelWarn
	// LevelIgnore means the error will be ignored
	LevelIgnore
)

// LevelMap indicates the map from `ErrGroup` to `Level`
type LevelMap [errGroupCount]Level

// Context defines how to handle an error
type Context struct {
	levelMap    LevelMap
	warnHandler contextutil.WarnAppender
}

// LevelMap returns the `levelMap` of the context.
func (ctx *Context) LevelMap() LevelMap {
	return ctx.levelMap
}

// LevelForGroup returns the level for a specified group.
func (ctx *Context) LevelForGroup(errGroup ErrGroup) Level {
	return ctx.levelMap[errGroup]
}

// WithStrictErrGroupLevel makes the context to return the error directly for any kinds of errors.
func (ctx *Context) WithStrictErrGroupLevel() Context {
	newCtx := Context{
		warnHandler: ctx.warnHandler,
	}

	return newCtx
}

// WithErrGroupLevel sets a `Level` for an `ErrGroup`
func (ctx *Context) WithErrGroupLevel(eg ErrGroup, l Level) Context {
	newCtx := Context{
		levelMap:    ctx.levelMap,
		warnHandler: ctx.warnHandler,
	}
	newCtx.levelMap[eg] = l

	return newCtx
}

// WithErrGroupLevels sets `levelMap` for an `ErrGroup`
func (ctx *Context) WithErrGroupLevels(levels LevelMap) Context {
	return Context{
		levelMap:    levels,
		warnHandler: ctx.warnHandler,
	}
}

// AppendWarning appends the error to warning. If the inner `warnHandler` is nil, do nothing.
func (ctx *Context) AppendWarning(err error) {
	intest.Assert(ctx.warnHandler != nil)
	if w := ctx.warnHandler; w != nil {
		// warnHandler should always not be nil, check fn != nil here to just make code safe.
		w.AppendWarning(err)
	}
}

// AppendNote appends the error to warning with level 'Note'. If the inner `warnHandler` is nil, do nothing.
func (ctx *Context) AppendNote(err error) {
	intest.Assert(ctx.warnHandler != nil)
	if w := ctx.warnHandler; w != nil {
		// warnHandler should always not be nil, check fn != nil here to just make code safe.
		w.AppendNote(err)
	}
}

// HandleError handles the error according to the contextutil. See the comment of `HandleErrorWithAlias` for detailed logic.
//
// It also allows using `errors.ErrorGroup`, in this case, it'll handle each error in order, and return the first error
// it founds.
func (ctx *Context) HandleError(err error) error {
	if err == nil {
		return nil
	}
	// The function of handling `errors.ErrorGroup` is placed in `HandleError` but not in `HandleErrorWithAlias`, because
	// it's hard to give a proper error and warn alias for an error group.
	if errs, ok := err.(errors.ErrorGroup); ok {
		for _, singleErr := range errs.Errors() {
			singleErr = ctx.HandleError(singleErr)
			// If the one error is found, just return it.
			// TODO: consider whether it's more appropriate to continue to handle other errors. For example, other errors
			// may need to append warnings. The current behavior is same with TiDB original behavior before using
			// `errctx` to handle multiple errors.
			if singleErr != nil {
				return singleErr
			}
		}

		return nil
	}

	return ctx.HandleErrorWithAlias(err, err, err)
}

// HandleErrorWithAlias handles the error according to the contextutil.
//  1. If the `internalErr` is not `"pingcap/errors".Error`, or the error code is not defined in the `errGroupMap`, or the error
//     level is set to `LevelError`(0), the `err` will be returned directly.
//  2. If the error level is set to `LevelWarn`, the `warnErr` will be appended as a warning.
//  3. If the error level is set to `LevelIgnore`, this function will return a `nil`.
//
// In most cases, these three should be the same. If there are many different kinds of error internally, but they are expected
// to give the same error to users, the `err` can be different form `internalErr`. Also, if the warning is expected to be
// different from the initial error, you can also use the `warnErr` argument.
//
// TODO: is it good to give an error code for internal only errors? Or should we use another way to distinguish different
// group of errors?
// TODO: both `types.Context` and `errctx.Context` can handle truncate error now. Refractor them.
func (ctx *Context) HandleErrorWithAlias(internalErr error, err error, warnErr error) error {
	if internalErr == nil {
		return nil
	}

	internalErr = errors.Cause(internalErr)

	e, ok := internalErr.(*errors.Error)
	if !ok {
		return err
	}

	eg, ok := errGroupMap[e.Code()]
	if !ok {
		return err
	}

	switch ctx.levelMap[eg] {
	case LevelError:
		return err
	case LevelWarn:
		ctx.AppendWarning(warnErr)
	case LevelIgnore:
	}

	return nil
}

// NewContext creates an error context to handle the errors and warnings
func NewContext(handler contextutil.WarnAppender) Context {
	return NewContextWithLevels(LevelMap{}, handler)
}

// NewContextWithLevels creates an error context to handle the errors and warnings
func NewContextWithLevels(levels LevelMap, handler contextutil.WarnAppender) Context {
	intest.Assert(handler != nil)
	return Context{
		warnHandler: handler,
		levelMap:    levels,
	}
}

// StrictNoWarningContext returns all errors directly, and ignore all errors
var StrictNoWarningContext = NewContext(contextutil.IgnoreWarn)

var errGroupMap = make(map[errors.ErrCode]ErrGroup)

// ErrGroup groups the error according to the behavior of handling errors
type ErrGroup int

const (
	// ErrGroupTruncate is the group of truncated errors
	ErrGroupTruncate ErrGroup = iota
	// ErrGroupDupKey is the group of duplicate key errors
	ErrGroupDupKey
	// ErrGroupBadNull is the group of bad null errors
	ErrGroupBadNull
	// ErrGroupNoDefault is the group of no default value errors
	ErrGroupNoDefault
	// ErrGroupDividedByZero is the group of divided by zero errors
	ErrGroupDividedByZero
	// ErrGroupAutoIncReadFailed is the group of auto increment read failed errors
	ErrGroupAutoIncReadFailed
	// ErrGroupNoMatchedPartition is the group of no partition is matched errors.
	ErrGroupNoMatchedPartition
	// errGroupCount is the count of all `ErrGroup`. Please leave it at the end of the list.
	errGroupCount
)

func init() {
	group2Errors := map[ErrGroup][]errors.ErrCode{
		ErrGroupTruncate: {
			errno.ErrTruncatedWrongValue,
			errno.ErrDataTooLong,
			errno.ErrTruncatedWrongValueForField,
			errno.ErrWarnDataOutOfRange,
			errno.ErrDataOutOfRange,
			errno.ErrBadNumber,
			errno.ErrWrongValueForType,
			errno.ErrDatetimeFunctionOverflow,
			errno.WarnDataTruncated,
			errno.ErrIncorrectDatetimeValue,
		},
		ErrGroupBadNull: {
			errno.ErrBadNull,
			errno.ErrWarnNullToNotnull,
		},
		ErrGroupNoDefault: {
			errno.ErrNoDefaultForField,
		},
		ErrGroupDividedByZero: {
			errno.ErrDivisionByZero,
		},
		ErrGroupAutoIncReadFailed: {
			errno.ErrAutoincReadFailed,
		},
		ErrGroupNoMatchedPartition: {
			errno.ErrNoPartitionForGivenValue,
			errno.ErrRowDoesNotMatchGivenPartitionSet,
		},
		ErrGroupDupKey: {
			errno.ErrDupEntry,
		},
	}

	for group, codes := range group2Errors {
		for _, errCode := range codes {
			errGroupMap[errCode] = group
		}
	}
}

// ResolveErrLevel resolves the error level according to the `ignore` and `warn` flags
// if ignore is true, it will return `LevelIgnore` to ignore the error,
// otherwise, it will return `LevelWarn` or `LevelError` according to the `warn` flag
// Only one of `ignore` and `warn` can be true.
func ResolveErrLevel(ignore bool, warn bool) Level {
	if ignore {
		return LevelIgnore
	}
	if warn {
		return LevelWarn
	}
	return LevelError
}
