package bizutils

import (
	"context"
	"fmt"
	"strings"

	"github.com/golang/protobuf/proto"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/metrics-integration/global/internal/domain/consts"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/account/accountdb"
	"github.com/tidbcloud/metrics-integration/global/pkg/log"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"go.uber.org/zap"
	"google.golang.org/grpc/metadata"
)

func GetClientIP(ctx context.Context) (string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", errcode.NewInternalErr(errors.New("failed to invoke FromIncomingContext"))
	}

	rips := md.Get("X-Forwarded-For")
	if len(rips) == 0 {
		return "", errcode.NewInternalErr(errors.New("remote real addr is empty"))
	}
	return parseXForwardedFor(rips)
}

// parseXForwardedFor follow: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#syntax
func parseXForwardedFor(headers []string) (string, error) {
	if len(headers) == 0 {
		return "", fmt.Errorf("no header: X-Forwarded-For")
	}
	// there should only be one header
	header := headers[0]
	for _, each := range strings.Split(header, ",") {
		return strings.TrimSpace(each), nil
	}

	return "", nil
}

// GetUserFromContext gets the tenant.User from context if exists.
func GetUserFromContext(ctx context.Context) (u *accountdb.User, err error) {
	claimsIFace := ctx.Value(auth0.AuthTokenClaimKey)
	if claimsIFace == nil {
		return nil, errors.New("user not in context")
	}
	user, ok := claimsIFace.(*accountdb.User)
	if !ok {
		return nil, errors.New("expect type accountdb.User in context")
	}
	return user, nil
}

// GetUIDOrZeroFromContext gets the current users' UID from context if exists, or returns 0.
func GetUIDOrZeroFromContext(ctx context.Context) uint64 {
	logger := log.GetLogger(ctx)

	u, err := GetUserFromContext(ctx)
	if err != nil {
		logger.Warn("get current user failed", zap.Error(err))
		return 0
	}

	return u.ID
}

// GetUserFromContext gets the tenant.User from context if exists.
func GetAuthTypeFromContext(ctx context.Context) string {
	authType := ctx.Value(auth0.AuthTypeKey)
	if authType == nil {
		return consts.GmailAuthType
	}
	r, ok := authType.(string)
	if !ok {
		return consts.GmailAuthType
	}
	return r
}

func GetApiOperatorIPFromContext(ctx context.Context) string {
	ip := ctx.Value(consts.ApiOperatorIP)
	if ip == nil {
		return ""
	}
	r, ok := ip.(string)
	if !ok {
		return ""
	}
	return r
}

type dataflowContextKey string

const (
	mutableErrorDetailsContextKey dataflowContextKey = "Dataflow/MutableErrorDetailsContextKey"
)

type mutableErrorDetails struct {
	// ErrorDetails stores all the detail information for errors.
	// TODO: currently the value is manipulated in serial,
	// if later we need to manipulate it in parallel, change it to an atomic.Value, or add a lock.
	ErrorDetails []proto.Message
}

func getMutableErrorDetailsFromContext(ctx context.Context) *mutableErrorDetails {
	val := ctx.Value(mutableErrorDetailsContextKey)
	if val == nil {
		return nil
	}
	mDetails, ok := val.(*mutableErrorDetails)
	if !ok {
		return nil
	}
	return mDetails
}

func WithInitMutableErrorDetails(ctx context.Context) context.Context {
	return context.WithValue(ctx, mutableErrorDetailsContextKey, &mutableErrorDetails{})
}

func AppendErrorDetails(ctx context.Context, details ...proto.Message) error {
	mDetails := getMutableErrorDetailsFromContext(ctx)
	if mDetails == nil {
		return errors.New("mutable error details are not initialized in the context")
	}
	mDetails.ErrorDetails = append(mDetails.ErrorDetails, details...)
	return nil
}

func GetErrorDetailsFromContext(ctx context.Context) []proto.Message {
	mDetails := getMutableErrorDetailsFromContext(ctx)
	if mDetails == nil {
		return nil
	}
	return mDetails.ErrorDetails
}
