package auth

import (
	"context"
	"errors"
	"strconv"
	"strings"

	"github.com/form3tech-oss/jwt-go"
	"github.com/pingcap/errcode"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/auth/util"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"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"
)

// The frontend relies on the error message, if you modify the content, you need to synchronize with the frontend.
const (
	// UnregisteredUserErrMsg The custome-email user has not registered in the system
	UnregisteredUserErrMsg = "unregistered user"

	// UnregisteredSsoUserErrMsg The sso user has not registered in the system
	UnregisteredSsoUserErrMsg = "unregistered sso user"

	// InactiveUserErrMsg User has been deleted
	InactiveUserErrMsg = "inactive user"

	// PendingUserErrMsg The user did not accept the invitation in the email
	PendingUserErrMsg = "pending user"

	// PendingSignupUserErrMsg The user has self-registered and has not activated the account in the email
	PendingSignupUserErrMsg = "pending signup user"

	// ImpersonatedUserErrMsg The impersonated can only read
	ImpersonatedUserErrMsg = "impersonated user is a read only user"
)

var consoleApiSingleton *consoleApi

func init() {
	consoleApiSingleton = &consoleApi{
		accountFacade: facade_imp.NewAccountFacade(),
	}
}

type consoleApi struct {
	accountFacade facade.AccountFacade
}

func (c *consoleApi) Auth(req AuthReq) (context.Context, error) {
	ctx := req.ctx
	logger := log.WithContextL(ctx)
	url := grpc_http.HttpURI(ctx)

	orgId, found := grpc_http.GetOrgIdFromPath(url)
	rawToken, err := getBearerToken(ctx)
	if err != nil {
		logger.Error("failed to get bearer token", zap.Error(err))
		return ctx, errcode.NewNotAuthenticatedErr(err)
	}

	// try to auth test user first
	testUser := req.testTokenChecker.TryGetTestUserFromToken(ctx, c.accountFacade, rawToken, orgId, !found)
	if testUser != nil {
		return wrapContextWithUser(ctx, testUser), nil
	}

	// auth the web console user
	claims, err := req.tokenValidator.ValidateFor(auth0.AuthValidatorIDManagement, rawToken)
	if err != nil {
		logger.Error("failed to validate token", zap.Error(err))
		return ctx, errcode.NewNotAuthenticatedErr(err)
	}

	user, err := c.queryUser(ctx, claims, orgId, !found)
	if err != nil {
		logger.Error("failed to query user", zap.Error(err))
		return ctx, err
	}

	if err := checkUserStatus(user); err != nil {
		logger.Error("failed to check user status", zap.Error(err))
		return ctx, err
	}

	// auth the impersonated user
	if user.IsImpersonated {
		if err := authImpersonatedUser(ctx); err != nil {
			logger.Error("failed to auth impersonated user", zap.Error(err))
			return ctx, err
		}
	}

	return wrapContextWithUser(ctx, user), nil
}

func wrapContextWithUser(ctx context.Context, user *facade.User) context.Context {
	newCtx := context.WithValue(ctx, auth0.AuthTypeKey, config.MailAuthType)
	newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, user)
	newCtx = context.WithValue(newCtx, auth0.AuthUserIDKey, user.Email)
	newCtx = context.WithValue(newCtx, config.CtxIdentityKey, strconv.FormatUint(user.ID, 10))
	newCtx = context.WithValue(newCtx, config.CtxGRPCGatewayTokenKey, config.CtxGRPCGatewayTokenValue)
	newCtx = context.WithValue(newCtx, config.CtxIdentityTypeKey, auth.AuthnIdentityTypeUserToken)
	newCtx = context.WithValue(newCtx, config.CtxIdentityValKey, strconv.FormatUint(user.ID, 10))
	return newCtx
}

func getBearerToken(ctx context.Context) (string, error) {
	bearToken := grpc_http.FirstHeader(ctx, "authorization")
	if bearToken != "" {
		tokenRaw, err := util.ExtractBearerToken(bearToken)
		if err != nil {
			return "", err
		}
		return tokenRaw, nil
	}

	return grpc_http.Query(ctx, "jwt"), nil
}

func getEmailFromClaims(claims jwt.MapClaims) (string, error) {
	email, err := auth0.GetEmailClaim(claims)
	if err == nil {
		return email, nil
	}

	email, err = auth0.GetCustomEmailClaim(claims)
	if err != nil {
		return "", errcode.NewNotAuthenticatedErr(err)
	}
	return email, nil
}

func (c *consoleApi) queryUser(
	ctx context.Context, claims jwt.MapClaims, orgID uint64, skipCheckOrg bool,
) (*facade.User, error) {
	email, err := getEmailFromClaims(claims)
	if err != nil {
		return nil, err
	}

	user, found, err := c.accountFacade.GetAuthUserInOrg(ctx, email, orgID, skipCheckOrg)
	if err != nil {
		return nil, err
	}
	if !found {
		subClaim, err := auth0.GetSubjectClaim(claims)
		if err != nil {
			return nil, errcode.NewNotAuthenticatedErr(err)
		}

		if IsSubSSO(subClaim) {
			return nil, errcode.NewNotAuthenticatedErr(errors.New(UnregisteredSsoUserErrMsg))
		}
		return nil, errcode.NewNotAuthenticatedErr(errors.New(UnregisteredUserErrMsg))
	}

	return user, nil
}

// IsSubSSO checks if the "sub" claim of auth0 token is sso
func IsSubSSO(subClaim string) bool {
	return strings.HasPrefix(subClaim, config.Auth0SocialConnectionGoogle.String()) ||
		strings.HasPrefix(subClaim, config.Auth0SocialConnectionGithub.String())
}

func checkUserStatus(user *facade.User) error {
	if user.Tenant != nil &&
		(user.Tenant.Status == constant.TenantStatusCleared || user.Tenant.Status == constant.TenantStatusDeleted) {
		return errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
	}

	switch user.Status {
	case constant.UserStatusActive:
		return nil
	case constant.UserStatusPending:
		if user.InvitedAt != nil {
			return errcode.NewNotAuthenticatedErr(errors.New(PendingUserErrMsg))
		}
		if user.Email == user.Tenant.Email && user.Tenant.Status == constant.TenantStatusPending {
			return errcode.NewNotAuthenticatedErr(errors.New(PendingSignupUserErrMsg))
		}
		return errcode.NewNotAuthenticatedErr(errors.New(PendingUserErrMsg))
	case constant.UserStatusInactive:
		return errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
	default:
		return errcode.NewNotAuthenticatedErr(errors.New(InactiveUserErrMsg))
	}
}

func authImpersonatedUser(ctx context.Context) error {
	url := grpc_http.HttpURI(ctx)
	var method string
	httpMethodFromContext := ctx.Value(auth0.AuthHttpMethod)
	if httpMethodFromContext != nil {
		method = strings.ToUpper(httpMethodFromContext.(string))
	} else {
		method = strings.ToUpper(grpc_http.HttpMethod(ctx))
	}
	if FilterHttpRequest(url, method) {
		return errcode.NewForbiddenErr(errors.New(ImpersonatedUserErrMsg))
	}
	return nil
}
