package xerr

import (
	"errors"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const errorDomain = "hyapp"

// GRPCCode 把稳定业务 reason 映射为 gRPC 标准 code。
// gateway 后续只需要读取 gRPC code 和 ErrorInfo.reason 即可生成 HTTP envelope。
func GRPCCode(code Code) codes.Code {
	return SpecOf(code).GRPCCode
}

// ToGRPCError 只在 transport/grpc 层调用。
// 如果传入的错误已经是 gRPC status，会原样返回，避免重复包裹健康检查等基础错误。
func ToGRPCError(err error) error {
	if err == nil {
		return nil
	}

	if _, ok := status.FromError(err); ok {
		return err
	}

	domainErr, ok := As(err)
	if !ok {
		domainErr = &Error{Code: Internal, Message: "internal error"}
	}

	st := status.New(GRPCCode(domainErr.Code), domainErr.Message)
	detail := &errdetails.ErrorInfo{
		Reason: string(domainErr.Code),
		Domain: errorDomain,
	}

	withDetails, detailErr := st.WithDetails(detail)
	if detailErr != nil {
		return st.Err()
	}

	return withDetails.Err()
}

// ReasonFromGRPC 供 gateway 或测试从 gRPC error detail 中恢复稳定 reason。
func ReasonFromGRPC(err error) Code {
	if err == nil {
		return ""
	}

	st, ok := status.FromError(err)
	if !ok {
		return Internal
	}

	for _, detail := range st.Details() {
		info, ok := detail.(*errdetails.ErrorInfo)
		if ok && info.GetReason() != "" {
			return Code(info.GetReason())
		}
	}

	return codeFromGRPC(st.Code())
}

// codeFromGRPC 是没有 ErrorInfo detail 时的降级映射。
func codeFromGRPC(code codes.Code) Code {
	switch code {
	case codes.InvalidArgument:
		return InvalidArgument
	case codes.NotFound:
		return NotFound
	case codes.Unauthenticated:
		return Unauthorized
	case codes.PermissionDenied:
		return PermissionDenied
	case codes.Unavailable, codes.DeadlineExceeded:
		// 下游服务未启动、网络不可达或 gRPC deadline 到期都属于依赖暂不可用；gateway 应返回 UPSTREAM_ERROR，
		// 不能降级为 INTERNAL_ERROR，否则本地联调和线上排障都会误判成业务代码内部崩溃。
		return Unavailable
	case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
		return Conflict
	default:
		return Internal
	}
}

// IsCode 帮助测试或 transport 分支判断错误 reason。
func IsCode(err error, code Code) bool {
	if err == nil {
		return false
	}

	if typed, ok := As(err); ok {
		return typed.Code == code
	}

	if _, ok := status.FromError(err); ok {
		return ReasonFromGRPC(err) == code
	}

	return errors.Is(err, &Error{Code: code})
}
