package errorcode

import (
	"errors"

	"github.com/tidbcloud/pkgv2/bizerr"
)

// ClusterSQLErrorHolder is implemented by errors that carry a raw SQL error
// (error number + message) raised from a user's TiDB cluster. It is defined as
// an interface here (rather than importing the concrete facade.ClusterProxyErr)
// to avoid an import cycle: the facade package already imports this package.
type ClusterSQLErrorHolder interface {
	// ClusterSQLError returns the underlying SQL error number and message.
	ClusterSQLError() (errno uint16, message string)
}

type userActionableClusterSQLErrorHolder interface {
	ClusterSQLErrorHolder
	userActionableClusterSQLError()
}

type userActionableClusterSQLError struct {
	bizerr.BizError
	holder ClusterSQLErrorHolder
}

func (e *userActionableClusterSQLError) ClusterSQLError() (uint16, string) {
	return e.holder.ClusterSQLError()
}

func (*userActionableClusterSQLError) userActionableClusterSQLError() {}

func (e *userActionableClusterSQLError) Unwrap() error {
	return e.BizError
}

// MarkClusterSQLErrorUserActionable records that the failed SQL operation was
// directly driven by a user request. Transport and service-internal SQL errors
// must not be marked: the same errno can otherwise be misreported as a 4xx.
func MarkClusterSQLErrorUserActionable(err error) bizerr.BizError {
	if err == nil {
		return nil
	}
	var be bizerr.BizError
	if !errors.As(err, &be) {
		return bizerr.WrapErrorWithCode(err, ErrCodeSvcClusterProxy, "cluster SQL operation")
	}
	var marked userActionableClusterSQLErrorHolder
	if errors.As(err, &marked) {
		return be
	}
	var holder ClusterSQLErrorHolder
	if !errors.As(err, &holder) {
		return be
	}
	return &userActionableClusterSQLError{BizError: be, holder: holder}
}

// userVisibleSQLErrorMessages is both an allow-list and a safe-message map.
// Raw TiDB messages are deliberately not returned: depending on the error
// number they may contain account names, hosts, SQL text, or customer data
// values (for example duplicate-key and invalid-value errors).
var userVisibleSQLErrorMessages = map[uint16]string{
	1007: "The database already exists.",
	1008: "The database does not exist.",
	1048: "A required column cannot be null.",
	1049: "The requested database does not exist.",
	1050: "The table already exists.",
	1051: "The requested table does not exist.",
	1052: "A referenced column is ambiguous.",
	1054: "A referenced column does not exist.",
	1059: "A database object name is too long.",
	1060: "The table definition contains a duplicate column name.",
	1061: "The table already has an index with the requested name.",
	1062: "A row with the same unique key already exists.",
	1063: "A column definition is invalid.",
	1064: "The SQL statement is invalid.",
	1071: "The requested index is too long.",
	1072: "An index references a column that does not exist.",
	1075: "The table has an invalid auto-increment definition.",
	1091: "The requested column or key does not exist.",
	1102: "The database name is invalid.",
	1109: "The referenced table does not exist.",
	1110: "A column is specified more than once.",
	1136: "The number of values does not match the number of columns.",
	1146: "The requested table does not exist.",
	1166: "A column name is invalid.",
	1167: "An index uses an invalid column.",
	1170: "A key length is required for a BLOB or TEXT column.",
	1171: "All columns in a primary key must be non-null.",
	1215: "The foreign key constraint could not be added.",
	1216: "The row cannot be changed because a foreign key constraint would be violated.",
	1217: "The row or table cannot be removed because other rows reference it.",
	1235: "The requested SQL feature is not supported.",
	1264: "A value is outside the valid range for its column.",
	1265: "A value was truncated for its column.",
	1305: "The requested function or procedure does not exist.",
	1364: "A required field has no default value.",
	1366: "A value is invalid for its column.",
	1396: "The requested user operation could not be completed.",
	1406: "A value is too long for its column.",
	1451: "The row cannot be changed because other rows reference it.",
	1452: "The row cannot be changed because a referenced row does not exist.",
	1690: "A value is outside the supported range.",
	3819: "A check constraint is violated.",
}

// FormatClusterSQLError inspects err for an underlying cluster SQL error and,
// if found, converts it into a user-facing BizError. The second return value
// reports whether err was a cluster SQL error and was handled.
//
//   - Explicitly marked user-actionable errors with an allow-listed number ->
//     a business-coded, fixed safe message (e.g. "The requested table does not
//     exist.").
//   - Unmarked errors and all other error numbers -> an internal-coded message
//     that still includes the error number for support/triage but reveals no
//     cluster internals.
func FormatClusterSQLError(err error) (bizerr.BizError, bool) {
	if err == nil {
		return nil, false
	}
	var holder ClusterSQLErrorHolder
	if !errors.As(err, &holder) {
		return nil, false
	}
	errno, _ := holder.ClusterSQLError()
	if errno == 0 {
		return nil, false
	}
	var userActionable userActionableClusterSQLErrorHolder
	if message, ok := userVisibleSQLErrorMessages[errno]; ok && errors.As(err, &userActionable) {
		return bizerr.New(ErrCodeClusterSQLError, "%s (error %d)", message, errno), true
	}
	return bizerr.New(
		ErrCodeInternalError,
		"Failed to access the TiDB cluster (error %d). Please retry; if the problem persists, contact support.",
		errno,
	), true
}
