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, "request cannot be completed in current state"),
	RoomClosed:      spec(codes.FailedPrecondition, httpStatusConflict, RoomClosed, "room closed"),
	RoomUserMuted:   spec(codes.PermissionDenied, httpStatusForbidden, RoomUserMuted, "You have been muted and cannot join the mic."),
	RoomRPSChallengeTaken: spec(
		codes.FailedPrecondition,
		httpStatusConflict,
		RoomRPSChallengeTaken,
		"手慢啦，该 PK 已被其他人接单",
	),
	RoomRPSChallengeExpired: spec(
		codes.FailedPrecondition,
		httpStatusConflict,
		RoomRPSChallengeExpired,
		"猜拳已过期",
	),

	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, "password already set"),
	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, "display user id already exists"),
	DisplayUserIDCooldown:           spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDCooldown, "display user id change is cooling down"),
	DisplayUserIDNotFound:           spec(codes.NotFound, httpStatusNotFound, DisplayUserIDNotFound, "not found"),
	DisplayUserIDAllocateFailed:     spec(codes.Internal, httpStatusInternalServerError, Internal, "internal error"),
	DisplayUserIDPrettyNotAvailable: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPrettyNotAvailable, "pretty display user id is not available"),
	DisplayUserIDPrettyActive:       spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPrettyActive, "pretty display user id already active"),
	DisplayUserIDLeaseExpired:       spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDLeaseExpired, "pretty display user id lease expired"),
	DisplayUserIDPaymentRequired:    spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPaymentRequired, "pretty display user id payment required"),

	CountryChangeCooldown: spec(codes.FailedPrecondition, httpStatusConflict, CountryChangeCooldown, "country change is cooling down"),
	CountryChangeIdentityLocked: spec(
		codes.PermissionDenied,
		httpStatusForbidden,
		CountryChangeIdentityLocked,
		"You already have a host identity. Please contact customer service to change your country.",
	),
	CountryNotFound:       spec(codes.NotFound, httpStatusNotFound, CountryNotFound, "not found"),
	RegionNotFound:        spec(codes.NotFound, httpStatusNotFound, RegionNotFound, "not found"),
	RegionCountryConflict: spec(codes.FailedPrecondition, httpStatusConflict, RegionCountryConflict, "country already belongs to another region"),
	RegionDisabled:        spec(codes.FailedPrecondition, httpStatusConflict, RegionDisabled, "region is disabled"),
	RegionMismatch:        spec(codes.PermissionDenied, httpStatusForbidden, RegionMismatch, "不是同一个地区"),
	InvalidInviteCode:     spec(codes.InvalidArgument, httpStatusBadRequest, InvalidInviteCode, "invalid invite code"),
	DeviceAlreadyRegistered: spec(
		codes.FailedPrecondition,
		httpStatusConflict,
		DeviceAlreadyRegistered,
		"device already registered",
	),
	CPAlreadyExistsSelf:  spec(codes.FailedPrecondition, httpStatusConflict, CPAlreadyExistsSelf, "You already have a CP"),
	CPAlreadyExistsOther: spec(codes.FailedPrecondition, httpStatusConflict, CPAlreadyExistsOther, "The other party already has a CP"),

	InsufficientBalance:     spec(codes.FailedPrecondition, httpStatusConflict, InsufficientBalance, "insufficient balance"),
	DuplicateBillingCommand: spec(codes.AlreadyExists, httpStatusConflict, DuplicateBillingCommand, "billing command already processed"),
	LedgerConflict:          spec(codes.Aborted, httpStatusConflict, LedgerConflict, "wallet state changed, please retry"),
	WalletResourceReadTransient: {
		GRPCCode:      codes.Unavailable,
		HTTPStatus:    httpStatusBadGateway,
		PublicCode:    string(WalletResourceReadTransient),
		PublicMessage: "wallet resource data is temporarily unavailable",
		Retryable:     true,
	},
	CoinSellerNotActive:            spec(codes.FailedPrecondition, httpStatusConflict, CoinSellerNotActive, "coin seller is not active"),
	CoinSellerStockTypeInvalid:     spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockTypeInvalid, "invalid argument"),
	CoinSellerStockAmountInvalid:   spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockAmountInvalid, "invalid argument"),
	CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "payment reference already used"),
	IdempotencyConflict:            spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "request payload does not match previous command"),
	PointWithdrawalLimitReached:    spec(codes.ResourceExhausted, 429, PointWithdrawalLimitReached, "withdrawal limit reached"),
	PointWithdrawalDateNotAllowed:  spec(codes.FailedPrecondition, httpStatusConflict, PointWithdrawalDateNotAllowed, "withdrawal date is not allowed"),
	VIPLevelNotFound:               spec(codes.NotFound, httpStatusNotFound, VIPLevelNotFound, "not found"),
	VIPLevelDisabled:               spec(codes.FailedPrecondition, httpStatusConflict, VIPLevelDisabled, "vip level is disabled"),
	VIPDowngradeNotAllowed:         spec(codes.FailedPrecondition, httpStatusConflict, VIPDowngradeNotAllowed, "vip downgrade is not allowed"),
	VIPProgramInactive:             spec(codes.FailedPrecondition, httpStatusConflict, VIPProgramInactive, "vip program is inactive"),
	VIPPackageNotPurchasable:       spec(codes.FailedPrecondition, httpStatusConflict, VIPPackageNotPurchasable, "vip package is not purchasable"),
	VIPInsufficientCoin:            spec(codes.FailedPrecondition, httpStatusConflict, VIPInsufficientCoin, "insufficient coin balance"),
	VIPTrialCardNotFound:           spec(codes.NotFound, httpStatusNotFound, VIPTrialCardNotFound, "vip trial card not found"),
	VIPTrialCardExpired:            spec(codes.FailedPrecondition, httpStatusConflict, VIPTrialCardExpired, "vip trial card expired"),
	VIPBenefitRequired:             spec(codes.PermissionDenied, httpStatusForbidden, VIPBenefitRequired, "vip benefit is required"),
	VIPGiftLevelRequired:           spec(codes.PermissionDenied, httpStatusForbidden, VIPGiftLevelRequired, "vip level is required for this gift"),
	VIPAntiKick:                    spec(codes.PermissionDenied, httpStatusForbidden, VIPAntiKick, "target user cannot be kicked"),
	VIPAntiMute:                    spec(codes.PermissionDenied, httpStatusForbidden, VIPAntiMute, "target user cannot be muted"),
	VIPRechargeRequired:            spec(codes.FailedPrecondition, httpStatusConflict, VIPRechargeRequired, "vip recharge is required"),
	VIPCoinRebateNotFound:          spec(codes.NotFound, httpStatusNotFound, VIPCoinRebateNotFound, "vip coin rebate not found"),
	VIPCoinRebateExpired:           spec(codes.FailedPrecondition, httpStatusConflict, VIPCoinRebateExpired, "vip coin rebate expired"),

	RedPacketDisabled:          spec(codes.FailedPrecondition, httpStatusConflict, RedPacketDisabled, "red packet is disabled"),
	RedPacketInvalidAmountTier: spec(codes.InvalidArgument, httpStatusBadRequest, RedPacketInvalidAmountTier, "invalid argument"),
	RedPacketInvalidCountTier:  spec(codes.InvalidArgument, httpStatusBadRequest, RedPacketInvalidCountTier, "invalid argument"),
	RedPacketAmountTooSmall:    spec(codes.InvalidArgument, httpStatusBadRequest, RedPacketAmountTooSmall, "invalid argument"),
	RedPacketDailyLimitReached: spec(codes.ResourceExhausted, 429, RedPacketDailyLimitReached, "rate limited"),
	RedPacketNotOpen:           spec(codes.FailedPrecondition, httpStatusConflict, RedPacketNotOpen, "red packet is not open"),
	RedPacketExpired:           spec(codes.FailedPrecondition, httpStatusConflict, RedPacketExpired, "red packet expired"),
	RedPacketAlreadyClaimed:    spec(codes.AlreadyExists, httpStatusConflict, RedPacketAlreadyClaimed, "red packet already claimed"),
	RedPacketSoldOut:           spec(codes.FailedPrecondition, httpStatusConflict, RedPacketSoldOut, "red packet sold out"),

	RuleNotActive:        spec(codes.FailedPrecondition, httpStatusConflict, RuleNotActive, "reward rule is not active"),
	EventAlreadyConsumed: spec(codes.AlreadyExists, httpStatusConflict, EventAlreadyConsumed, "event already consumed"),
	RewardPending:        spec(codes.FailedPrecondition, httpStatusConflict, RewardPending, "reward is pending"),

	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, "request payload does not match existing record"),
	ProducerEventConflict: spec(codes.FailedPrecondition, httpStatusConflict, ProducerEventConflict, "producer event payload does not match existing record"),
}

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
}
