package xerr

import "google.golang.org/grpc/codes"

const (
	httpStatusBadRequest          = 400
	httpStatusUnauthorized        = 401
	httpStatusForbidden           = 403
	httpStatusNotFound            = 404
	httpStatusConflict            = 409
	httpStatusInternalServerError = 500
	httpStatusBadGateway          = 502
)

// Spec 是错误码注册表中的一条稳定契约。
// GRPCCode 是服务间传输语义；PublicCode/PublicMessage 是 gateway 对 App 暴露的 envelope。
type Spec struct {
	GRPCCode      codes.Code
	HTTPStatus    int
	PublicCode    string
	PublicMessage string
	Retryable     bool
}

var catalog = map[Code]Spec{
	InvalidArgument: spec(codes.InvalidArgument, httpStatusBadRequest, InvalidArgument, "invalid argument"),
	NotFound:        spec(codes.NotFound, httpStatusNotFound, NotFound, "not found"),
	Conflict:        spec(codes.FailedPrecondition, httpStatusConflict, Conflict, "conflict"),
	RoomClosed:      spec(codes.FailedPrecondition, httpStatusConflict, RoomClosed, "room closed"),

	Unauthorized:     spec(codes.Unauthenticated, httpStatusUnauthorized, Unauthorized, "unauthorized"),
	PermissionDenied: spec(codes.PermissionDenied, httpStatusForbidden, PermissionDenied, "permission denied"),
	ProfileRequired:  spec(codes.PermissionDenied, httpStatusForbidden, ProfileRequired, "profile required"),
	Unavailable: {
		GRPCCode:      codes.Unavailable,
		HTTPStatus:    httpStatusBadGateway,
		PublicCode:    "UPSTREAM_ERROR",
		PublicMessage: "upstream service error",
		Retryable:     true,
	},
	Internal: spec(codes.Internal, httpStatusInternalServerError, Internal, "internal error"),

	AuthFailed:         spec(codes.Unauthenticated, httpStatusUnauthorized, AuthFailed, "authentication failed"),
	AuthLoginBlocked:   spec(codes.PermissionDenied, httpStatusForbidden, AuthLoginBlocked, "login blocked"),
	PasswordAlreadySet: spec(codes.AlreadyExists, httpStatusConflict, PasswordAlreadySet, "conflict"),
	UserDisabled:       spec(codes.PermissionDenied, httpStatusForbidden, UserDisabled, "permission denied"),
	SessionExpired:     spec(codes.Unauthenticated, httpStatusUnauthorized, SessionExpired, "unauthorized"),
	SessionRevoked:     spec(codes.Unauthenticated, httpStatusUnauthorized, SessionRevoked, "unauthorized"),

	DisplayUserIDInvalid:            spec(codes.InvalidArgument, httpStatusBadRequest, DisplayUserIDInvalid, "invalid argument"),
	DisplayUserIDExists:             spec(codes.AlreadyExists, httpStatusConflict, DisplayUserIDExists, "conflict"),
	DisplayUserIDCooldown:           spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDCooldown, "conflict"),
	DisplayUserIDNotFound:           spec(codes.NotFound, httpStatusNotFound, DisplayUserIDNotFound, "not found"),
	DisplayUserIDAllocateFailed:     spec(codes.Internal, httpStatusInternalServerError, Internal, "internal error"),
	DisplayUserIDPrettyNotAvailable: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPrettyNotAvailable, "conflict"),
	DisplayUserIDPrettyActive:       spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPrettyActive, "conflict"),
	DisplayUserIDLeaseExpired:       spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDLeaseExpired, "conflict"),
	DisplayUserIDPaymentRequired:    spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPaymentRequired, "conflict"),

	CountryChangeCooldown: spec(codes.FailedPrecondition, httpStatusConflict, CountryChangeCooldown, "conflict"),
	CountryNotFound:       spec(codes.NotFound, httpStatusNotFound, CountryNotFound, "not found"),
	RegionNotFound:        spec(codes.NotFound, httpStatusNotFound, RegionNotFound, "not found"),
	RegionCountryConflict: spec(codes.FailedPrecondition, httpStatusConflict, RegionCountryConflict, "conflict"),
	RegionDisabled:        spec(codes.FailedPrecondition, httpStatusConflict, RegionDisabled, "conflict"),
	InvalidInviteCode:     spec(codes.InvalidArgument, httpStatusBadRequest, InvalidInviteCode, "invalid argument"),

	InsufficientBalance:            spec(codes.FailedPrecondition, httpStatusConflict, InsufficientBalance, "insufficient balance"),
	DuplicateBillingCommand:        spec(codes.AlreadyExists, httpStatusConflict, DuplicateBillingCommand, "conflict"),
	LedgerConflict:                 spec(codes.Aborted, httpStatusConflict, LedgerConflict, "conflict"),
	CoinSellerNotActive:            spec(codes.FailedPrecondition, httpStatusConflict, CoinSellerNotActive, "conflict"),
	CoinSellerStockTypeInvalid:     spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockTypeInvalid, "invalid argument"),
	CoinSellerStockAmountInvalid:   spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockAmountInvalid, "invalid argument"),
	CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "conflict"),
	IdempotencyConflict:            spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "conflict"),
	VIPLevelNotFound:               spec(codes.NotFound, httpStatusNotFound, VIPLevelNotFound, "not found"),
	VIPLevelDisabled:               spec(codes.FailedPrecondition, httpStatusConflict, VIPLevelDisabled, "conflict"),
	VIPDowngradeNotAllowed:         spec(codes.FailedPrecondition, httpStatusConflict, VIPDowngradeNotAllowed, "conflict"),

	RuleNotActive:        spec(codes.FailedPrecondition, httpStatusConflict, RuleNotActive, "conflict"),
	EventAlreadyConsumed: spec(codes.AlreadyExists, httpStatusConflict, EventAlreadyConsumed, "conflict"),
	RewardPending:        spec(codes.FailedPrecondition, httpStatusConflict, RewardPending, "conflict"),

	InvalidSection:        spec(codes.InvalidArgument, httpStatusBadRequest, InvalidSection, "invalid argument"),
	MessageNotFound:       spec(codes.NotFound, httpStatusNotFound, MessageNotFound, "not found"),
	MessageRecalled:       spec(codes.NotFound, httpStatusNotFound, MessageRecalled, "not found"),
	PageTokenInvalid:      spec(codes.InvalidArgument, httpStatusBadRequest, PageTokenInvalid, "invalid argument"),
	RequestConflict:       spec(codes.FailedPrecondition, httpStatusConflict, RequestConflict, "conflict"),
	ProducerEventConflict: spec(codes.FailedPrecondition, httpStatusConflict, ProducerEventConflict, "conflict"),
}

func spec(grpcCode codes.Code, httpStatus int, publicCode Code, publicMessage string) Spec {
	return Spec{
		GRPCCode:      grpcCode,
		HTTPStatus:    httpStatus,
		PublicCode:    string(publicCode),
		PublicMessage: publicMessage,
	}
}

// SpecOf 返回业务码的注册表项。未知业务码降级为 INTERNAL_ERROR，避免泄漏未登记 reason。
func SpecOf(code Code) Spec {
	if registered, ok := catalog[code]; ok {
		return registered
	}

	return catalog[Internal]
}

// RegisteredCodes 返回当前注册表中全部业务码，供测试和文档生成使用。
func RegisteredCodes() []Code {
	registered := make([]Code, 0, len(catalog))
	for code := range catalog {
		registered = append(registered, code)
	}

	return registered
}
