package auth

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/metadata"
)

func WhetherRequestComeFromGrpcGateway(ctx context.Context) bool {
	token := GetHttpMetadataFromGrpcContext(ctx, GrpcGatewayMetadataToken)
	return token == GrpcGatewayMetadataVal
}

// AddHttpMetadata2GrpcContext add http path、 method、header、query
func AddHttpMetadata2GrpcContext(ctx context.Context, r *http.Request) metadata.MD {
	pmMd := AddHttpPathMethod2GrpcContext(ctx, r)
	qMd := AddHttpQuery2GrpcContext(ctx, r)
	hMd := AddHttpHeader2GrpcContext(ctx, r)
	return metadata.Join(pmMd, qMd, hMd)
}

// AddHttpHeader2GrpcContext authn need http header to identity the visitor
func AddHttpHeader2GrpcContext(_ context.Context, r *http.Request) metadata.MD {
	m := make(map[string]string)
	m[GrpcGatewayMetadataToken] = GrpcGatewayMetadataVal
	header := r.Header
	for k, v := range header {
		h := GrpcGatewayMetadataHeader + k
		m[strings.ToLower(h)] = v[0]
	}
	return metadata.New(m)
}

// AddHttpQuery2GrpcContext access need http query (auth_org_id) to adapt open api access
func AddHttpQuery2GrpcContext(_ context.Context, r *http.Request) metadata.MD {
	m := make(map[string]string)
	m[GrpcGatewayMetadataToken] = GrpcGatewayMetadataVal
	values := r.URL.Query()
	for k, v := range values {
		h := GrpcGatewayMetadataQuery + k
		m[strings.ToLower(h)] = v[0]
	}
	return metadata.New(m)
}

// AddHttpPathMethod2GrpcContext access need http path method to confirm the accessed resource_uri
func AddHttpPathMethod2GrpcContext(c context.Context, r *http.Request) metadata.MD {
	m := make(map[string]string)
	m[GrpcGatewayMetadataToken] = GrpcGatewayMetadataVal
	m[AccessMetadataPath] = r.URL.Path
	m[AccessMetadataMethod] = r.Method
	return metadata.New(m)
}

func GetHttpMetadataFromGrpcContext(ctx context.Context, header string) string {
	headers, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return ""
	}
	val := headers[header]
	if len(val) < 1 {
		return ""
	}
	return val[0]
}

func GetAccessOrgIdFromGinContext(c *gin.Context) (uint64, error) {
	queries := c.Request.URL.Query()
	if len(queries) == 0 {
		return 0, nil
	}
	accessOrgIdArr, ok := queries[AccessOrgIdQueryKey]
	if !ok {
		return 0, nil
	}
	accessOrgIdStr := accessOrgIdArr[0]
	return strconv.ParseUint(accessOrgIdStr, 10, 64)
}

func GetAccessOrgIdFromGrpcContext(ctx context.Context) (uint64, error) {
	key := GrpcGatewayMetadataQuery + strings.ToLower(AccessOrgIdQueryKey)
	val := GetHttpMetadataFromGrpcContext(ctx, key)
	if val == "" {
		return 0, nil
	}
	return strconv.ParseUint(val, 10, 64)
}

func GetIdentityInfoFromGinContext(c *gin.Context) (*AuthnIdentityInfo, error) {
	identityI, exists := c.Get(IdentityInfoKey)
	if !exists {
		return nil, errors.New("not found `authn_identity_info_key` in gin context keys")
	}
	info, ok := identityI.(*AuthnIdentityInfo)
	if !ok {
		return nil, errors.New("`authn_identity_info_key` relate identity info is not type of *auth.AuthnIdentityInfo")
	}
	return info, nil
}

func GetIdentityInfoFromGrpcContext(ctx context.Context) (*AuthnIdentityInfo, error) {
	value := ctx.Value(IdentityInfoKey)
	if value == nil {
		return nil, errors.New("not found `authn_identity_info_key` in go context")
	}
	info, ok := value.(*AuthnIdentityInfo)
	if !ok {
		return nil, errors.New("`authn_identity_info_key` relate identity info is not type of *auth.AuthnIdentityInfo")
	}
	return info, nil
}

func GinGetIdentityFunc(ctx context.Context, param *AccessParam) (*IdentityRet, error) {
	c := param.Req.(*gin.Context)
	info, err := GetIdentityInfoFromGinContext(c)
	if err != nil {
		return nil, err
	}

	return convAuthnIdentityInfo2IdentityRet(info)
}

func GrpcGatewayGetIdentityFunc(ctx context.Context, param *AccessParam) (*IdentityRet, error) {
	info, err := GetIdentityInfoFromGrpcContext(ctx)
	if err != nil {
		return nil, err
	}

	return convAuthnIdentityInfo2IdentityRet(info)
}

func convAuthnIdentityInfo2IdentityRet(info *AuthnIdentityInfo) (*IdentityRet, error) {
	var identityTye AccessIdentityType
	var identityVal string

	if info.IdentityType == AuthnIdentityTypeAccessToken {
		rsp := info.AccessTokenRsp
		identityTye = AccessIdentityTypeAccessToken
		identityVal = rsp.AccessTokenIdentity.RawAccessToken

	} else if info.IdentityType == AuthnIdentityTypeApiKeyAK {
		rsp := info.ApiKeyRsp
		identityTye = AccessIdentityTypeApiKeyAK
		identityVal = rsp.ApiKey.AccessKey

	} else if info.IdentityType == AuthnIdentityTypeUserToken {
		rsp := info.UserTokenRsp
		identityTye = AccessIdentityTypeUserID
		identityVal = fmt.Sprintf("%d", rsp.User.Id)
	} else {
		return nil, errors.New(fmt.Sprintf("unkown AuthnIdentityType %s", info.IdentityType))
	}

	ret := IdentityRet{
		IdentityType:  identityTye,
		IdentityValue: identityVal,
	}
	return &ret, nil
}
