package xerr

import (
	"context"
	"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
	}

	// context 终止描述的是调用链生命周期，而不是业务内部故障。仓储层会直接返回
	// context.Canceled/DeadlineExceeded；若先走兜底 Internal，主动取消的并发 sibling
	// 会在服务端被误报成 INTERNAL_ERROR，并把一次正常的快速失败伪装成数据库事故。
	if errors.Is(err, context.Canceled) {
		return status.Error(codes.Canceled, context.Canceled.Error())
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return status.Error(codes.DeadlineExceeded, context.DeadlineExceeded.Error())
	}

	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,
		Metadata: cloneMetadata(domainErr.Metadata),
	}

	if withDetails, detailErr := st.WithDetails(detail); detailErr == nil {
		st = withDetails
	}

	return &grpcError{status: st, cause: err}
}

// grpcError 对 grpc-go 暴露脱敏后的 status，同时保留脱敏前的原始错误链。
// 客户端从 GRPCStatus/Error 只能看到 catalog 文案；原始链只有服务端进程内
// 通过 CauseFromGRPC 可达，不会进 wire。
type grpcError struct {
	status *status.Status
	cause  error
}

func (e *grpcError) Error() string {
	return e.status.Err().Error()
}

// GRPCStatus 让 status.FromError 和 grpc-go server 识别脱敏 status，而不是降级成 Unknown。
func (e *grpcError) GRPCStatus() *status.Status {
	return e.status
}

// Unwrap 保留原始错误链，供服务端 errors.Is/As 与 access 日志定位根因。
func (e *grpcError) Unwrap() error {
	return e.cause
}

// CauseFromGRPC 取回 ToGRPCError 脱敏前的原始错误链，仅用于服务端日志；
// 对已经是 gRPC status 的透传错误返回 nil。禁止把返回值写回给客户端。
func CauseFromGRPC(err error) error {
	var typed *grpcError
	if errors.As(err, &typed) {
		return typed.cause
	}
	return nil
}

// 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())
}

// MetadataFromGRPC 只读取本项目 ErrorInfo 中由 owner service 显式声明的安全字段。
// 其它 detail/domain 不属于公开 HTTP 契约，gateway 不得把它们混入 envelope.data。
func MetadataFromGRPC(err error) map[string]string {
	if err == nil {
		return nil
	}
	st, ok := status.FromError(err)
	if !ok {
		return nil
	}
	for _, detail := range st.Details() {
		info, ok := detail.(*errdetails.ErrorInfo)
		if ok && info.GetDomain() == errorDomain && info.GetReason() != "" {
			return cloneMetadata(info.GetMetadata())
		}
	}
	return nil
}

// 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})
}
