package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	grpc_http "github.com/tidbcloud/serverless-service/internal/grpc/http"
	"github.com/tidbcloud/serverless-service/internal/infra/facade_imp"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
)

var (
	openApiSingleton *openApi
	errorAuthFailed  = errors.New("auth failed")
)

func init() {
	openApiSingleton = &openApi{
		accountFacade: facade_imp.NewAccountFacade(),
	}
}

type openApi struct {
	accountFacade facade.AccountFacade
}

func (o *openApi) Auth(req AuthReq) (context.Context, error) {
	ctx := req.ctx
	identityType, identityValue, err := getOpenAPIIdentity(ctx)
	if err != nil {
		log.WithContextL(ctx).Warn("failed to get open api identity", zap.Error(err))
		return nil, errcode.NewNotAuthenticatedErr(errorAuthFailed)
	}

	var newCtx context.Context
	switch identityType {
	case auth.AuthnIdentityTypeApiKeyAK:
		newCtx, err = o.getApiKeyUser(ctx, identityValue)
		if err != nil {
			log.WithContextL(ctx).Warn("failed to get api key user", zap.Error(err))
			return nil, errcode.NewNotAuthenticatedErr(errorAuthFailed)
		}
	case auth.AuthnIdentityTypeAccessToken:
		newCtx, err = o.getAccessTokenIdentity(ctx, identityValue)
		if err != nil {
			log.WithContextL(ctx).Warn("failed to get access token identity", zap.Error(err))
			return nil, errcode.NewNotAuthenticatedErr(errorAuthFailed)
		}
	default:
		log.WithContextL(ctx).Warn("identity type invalid", zap.Any("type", identityType))
		return nil, errcode.NewNotAuthenticatedErr(errorAuthFailed)
	}

	newCtx = context.WithValue(newCtx, config.CtxIdentityTypeKey, identityType)
	newCtx = context.WithValue(newCtx, config.CtxIdentityValKey, identityValue)
	return newCtx, nil
}

func (o *openApi) getAccessTokenIdentity(ctx context.Context, identityValue string) (context.Context, error) {
	ret, err := auth.AuthnAccessToken(ctx, &auth.IdentityVal{
		Value: identityValue,
		Options: auth.IdentityAuthOption{
			EnableLog: true,
		},
	})
	if err != nil {
		return nil, err
	}
	if ret.AuthRet != auth.AuthnRetOK {
		return nil, errors.New(ret.AuthMessage)
	}
	if ret.IdentityInfo == nil || ret.IdentityInfo.AccessTokenRsp == nil ||
		ret.IdentityInfo.AccessTokenRsp.AccessTokenIdentity == nil {
		return nil, errors.New("access token identity invalid")
	}

	identity := ret.IdentityInfo.AccessTokenRsp.AccessTokenIdentity
	authUser, found, err := o.accountFacade.GetAuthUserInOrg(ctx, identity.UserEmail, identity.OrgId, false)
	if err != nil {
		return nil, err
	}
	if !found {
		return nil, errcode.NewNotAuthenticatedErr(errors.New(UnregisteredUserErrMsg))
	}

	newCtx := context.WithValue(ctx, auth0.AuthTypeKey, config.AccessTokenAuthType)
	newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, authUser)
	newCtx = context.WithValue(newCtx, auth0.AuthUserIDKey, identity.UserEmail)
	newCtx = context.WithValue(newCtx, config.CtxIdentityKey, strconv.FormatUint(identity.UserId, 10))
	newCtx = context.WithValue(newCtx, config.CtxGRPCGatewayTokenKey, config.CtxGRPCGatewayTokenValue)
	return newCtx, nil
}

func (o *openApi) getApiKeyUser(ctx context.Context, publicKey string) (context.Context, error) {
	oaAccess, err := o.accountFacade.GetOpenAPIAccess(ctx, publicKey)
	if err != nil {
		return nil, err
	}

	user, err := o.accountFacade.GetOrCreateSystemUser(ctx, oaAccess.OrgID)
	if err != nil {
		return nil, err
	}

	// the returned projects are all active projects in the org, not the authorized project
	// project authorization will be checked by RBAC
	for _, projectID := range oaAccess.Projects {
		user.Projects = append(user.Projects, facade.Project{
			ID: projectID,
		})
	}
	user.LastName = publicKey
	user.Email = "OpenAPI"

	newCtx := context.WithValue(ctx, auth0.AuthTypeKey, config.ApiKeyAuthType)
	newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, user)
	newCtx = context.WithValue(newCtx, auth0.AuthUserIDKey, user.Email)
	newCtx = context.WithValue(newCtx, config.CtxIdentityKey, publicKey)
	newCtx = context.WithValue(newCtx, config.CtxGRPCGatewayTokenKey, config.CtxGRPCGatewayTokenValue)
	newCtx = context.WithValue(newCtx, config.CtxOpenApiRoles, oaAccess.Roles)
	return newCtx, nil
}

func getOpenAPIIdentity(ctx context.Context) (auth.AuthnIdentityType, string, error) {
	authMethodHeader := grpc_http.FirstHeader(ctx, auth.XAuthMethodHeader)
	switch authMethodHeader {
	case auth.XAuthMethodValBear:
		authRawHeader := grpc_http.FirstHeader(ctx, auth.XAuthRawHeader)
		if authRawHeader == "" {
			return "", "", fmt.Errorf(
				"header %s is %s, but header %s is empty",
				auth.XAuthRawHeader,
				authMethodHeader,
				auth.XAuthRawHeader,
			)
		}
		identityType := auth.AuthnIdentityTypeAccessToken
		identityVal, err := parseIdentityValue(identityType, authRawHeader)
		return identityType, identityVal, err
	case auth.XAuthMethodValBasic, auth.XAuthMethodValDigest:
		authContentHeader := grpc_http.FirstHeader(ctx, auth.XAuthContentHeader)
		if authContentHeader == "" {
			return "", "", fmt.Errorf(
				"header %s is %s, but header %s is empty",
				auth.XAuthRawHeader,
				authMethodHeader,
				auth.XAuthContentHeader,
			)
		}
		identityType := auth.AuthnIdentityTypeApiKeyAK
		identityVal, err := parseIdentityValue(identityType, authContentHeader)
		return identityType, identityVal, err
	}

	return "", "", fmt.Errorf("unknown header %s:%s", auth.XAuthMethodHeader, authMethodHeader)
}

func parseIdentityValue(tye auth.AuthnIdentityType, header string) (string, error) {
	if tye == auth.AuthnIdentityTypeAccessToken {
		headerParts := strings.Split(header, " ")
		if len(headerParts) != 2 {
			return "", fmt.Errorf("access token header invalid: %s", header)
		}
		part0 := strings.ToLower(headerParts[0])
		if part0 == "bearer" {
			return headerParts[1], nil
		}

		return "", fmt.Errorf("access token header not starts with bearer, header: %s", header)
	}

	if tye == auth.AuthnIdentityTypeApiKeyAK {
		apiKey := new(auth.OpenApiKeyHeaderInfo)
		err := json.Unmarshal([]byte(header), apiKey)
		if err != nil {
			return "", fmt.Errorf("failed to unmarshal auth content: %s, error: %s", header, err)
		}
		if apiKey.PublicKey == "" {
			return "", fmt.Errorf("get ak from auth content is empty, auth content: %s", header)
		}
		return apiKey.PublicKey, nil
	}

	return "", fmt.Errorf("identity type %s invalid", tye)
}
